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

   Copyright (c) 2011-2018, 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.

   30-Sep-18    JDB     Replaced DMASK with D16_MASK
   03-Aug-17    JDB     Changed perror call for I/O errors to cprintf
   22-Apr-17    JDB     A failed sim_fseek call now causes a drive fault
   09-Mar-17    JDB     Added the simulator name to the "perror" message.
   17-Jan-17    JDB     Moved "hp2100_defs.h" inclusion to "hp2100_disclib.c"
   13-May-16    JDB     Modified for revised SCP API function parameter types
   04-Mar-16    JDB     Name changed to "hp2100_disclib" until HP 3000 integration
   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, Aug-1980)
   - HP 13365 Integrated Controller Programming Guide (13365-90901, Feb-1980)
   - HP 1000 ICD/MAC Disc Diagnostic Reference Manual (5955-4355, Jun-1984)
   - RTE-IVB System Manager's Manual (92068-90006, Jan-1983)
   - DVR32 RTE Moving Head Driver source (92084-18711, Revision 5000)


   This library provides 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 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.

   As a result of this separation, the library does not handle the data transfer
   between the controller and the interface directly.  Instead, data is moved
   between the interface and a sector buffer by the interface simulator, and
   then the buffer is passed to the disc library for reading or writing.  This
   buffer is also used to pass disc commands and parameters to the controller,
   and to receive status information from the controller.  Only one buffer is
   needed per interface, regardless of the number of controllers or units
   handled, as a single interface cannot perform data transfers concurrently
   with controller commands.

   The library provides routines to prepare, start, and end commands, service
   units, and poll drives for Attention status.  In addition, routines are
   provided to attach and detach disc images from drive units, load and unload
   disc heads, classify commands, and provide opcode and phase name strings for
   debugging.

   Autosizing is supported when attaching a disc image.  If enabled, the model
   of the drive is set to match the disc image size.  For example, if a 50 MB
   disc image is attached to a unit set for autosizing, the unit's model will be
   set to a 7920(H).

   The interface simulator declares a structure that contains the state
   variables for a controller.  A MAC controller may handle multiple disc units.
   An ICD controller handles only a single disc unit, but multiple controllers
   may be employed to support several drives on a given interface.  The type of
   the controller (MAC or ICD) is contained in the structure, which is passed to
   the disc library routines.  The minor differences in controller action
   between the two are handled internally.  A macro (CNTLR_INIT) is provided to
   initialize the structure.

   The interface simulator also declares the sector buffer.  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.  Other than setting the length when the controller places data into
   the buffer and resetting the index at the start of a sector read or write,
   the interface simulator is free to manipulate these values as desired.

   In general, a user of the library is free to read any of the controller state
   variable structure fields.  Writing to the fields generally will interfere
   with controller operations, with these exceptions:

     Field Name   Description
     ===========  ============================
     status       controller status
     eod          end of data flag
     index        data buffer index
     length       data buffer length
     seek_time    per-cylinder seek delay time
     sector_time  intersector delay time
     cmd_time     command response time
     data_time    data transfer response time
     wait_time    command wait time

   In hardware, the controller executes in three basic 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 checked for a
       command; if present, it is executed.  If none are pending, 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 connected 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 Poll Loop continues.

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

       In each pass of the loop, the current 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.8 second timer is running.  If it
       expires before a command is received, the file mask is reset, and the
       Poll Loop is entered.

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

       During command execution, the waits for input parameters, seek
       completion, data transfers, and output status words are handled
       internally.  Each wait is governed by the 1.8 second timer; if it
       expires, the command is aborted.

   In simulation, these states are represented by the values cntlr_idle,
   cntlr_wait, and cntlr_busy, respectively.

   A MAC controller operates from one to eight drives, represented by an array
   of one to eight units.  When operating multiple units, a pointer to the first
   unit of a contiguous array is passed, and the unit number present in the
   command is used to index to the target unit.

   A MAC controller emulation also requires an array of two contiguous auxiliary
   units containing a controller unit and a command wait timeout unit.  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 limits the amount of time the controller
   will wait for the interface to supply a command or parameter.  A pointer to
   the auxiliary unit array is set up during controller state variable
   initialization.  The auxiliary array may be separate or an extension of the
   drive unit array.

   An ICD controller manages a single unit corresponding to the drive in which
   the controller is integrated.  An interface declares a unit array
   corresponding to the number of drives supported and passes the unit number to
   use to the command preparation and start routines.  Auxiliary units are not
   used, and all commands are scheduled on the drive unit associated with a
   given controller.

   The library provides a unit service routine to handle all of the disc
   commands.  The routine is called from the interface service routine to handle
   the common disc actions, while the interface routine handles actions specific
   to the operation of the interface (such as data transfer).

   The service routine schedules the unit to continue command execution under
   these conditions:

     1. A Seek or Recalibrate command is waiting for the seek completion.

     2. A read or write command is waiting for the first data transfer of a
        sector to start.

     3. A read or write command is waiting for the next sector to start after
        the final data transfer of the preceding sector.

     4. A Verify command is waiting for the end of the current sector.

   The library also provides controller and timer service routines for MAC
   emulations.  All three (unit, controller, and timer) must be called from
   their respective interface service routines before any interface-specific
   actions, if any, are taken.

   On return from the library unit or controller service routines, the "wait"
   field of the UNIT structure will be set to the activation time if the unit
   is to be scheduled.  The caller is responsible for activating the unit.  If
   the caller uses this feature, the field should be reset to zero before the
   next service call.

   The MAC timer unit is activated by the library, and its "wait" field is not
   used.  The timer starts when a command other than End, Seek, or Recalibrate
   completes, or when the controller is waiting for the interface to supply or
   accept a parameter during command execution.  It stops when an End, Seek, or
   Recalibrate command completes, a command is prepared for execution, or the
   final parameter has been supplied or accepted by the interface during command
   execution.

   The controller maintains six variables in each drive's unit structure:

     wait       -- the current service activation time
     pos        -- the current byte offset into the disc image file
     u3 (CYL)   -- the current drive cylinder
     u4 (STAT)  -- the drive status (Status-2)
     u5 (OP)    -- the drive operation in process
     u6 (PHASE) -- the current operation phase

   These and other definitions are in the file hp_disclib.h, which must be
   included in the interface simulator.

   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.


   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 library does not simulate interface signals or function bus orders,
       except for EOD (End of Data) and BUSY.  The interface simulators must
       decide for themselves what actions to take (e.g., interrupting the CPU)
       on the basis of the controller state.

    4. The command/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.

    5. The SAVE command does not save the "wait" and "pos" fields of the UNIT
       structure automatically.  To ensure that they are saved, they are
       referenced by hidden, read-only registers.
*/



#include <math.h>

#include "hp2100_defs.h"                                /* this must reflect the machine used */
#include "hp2100_disclib.h"



/* Command accessors */

#define DL_V_OPCODE      8                              /* bits 12- 8: general opcode */
#define DL_V_HOLD        7                              /* bits  7- 7: general hold flag */
#define DL_V_UNIT        0                              /* bits  3- 0: general unit number */

#define DL_V_SPD        13                              /* bits 15-13: Initialize S/P/D flags */
#define DL_V_CHEAD       6                              /* bits  7- 6: Cold Load Read head number */
#define DL_V_CSECT       0                              /* bits  5- 0: Cold Load Read sector number */
#define DL_V_FRETRY      4                              /* bits  7- 4: Set File Mask retry count */
#define DL_V_FDECR       3                              /* bits  3- 3: Set File Mask seek decrement */
#define DL_V_FSPEN       2                              /* bits  2- 2: Set File Mask sparing enable */
#define DL_V_FCYLM       1                              /* bits  1- 1: Set File Mask cylinder mode */
#define DL_V_FAUTSK      0                              /* bits  0- 0: Set File Mask auto seek */

#define DL_V_FMASK       0                              /* bits  3- 0: Set File Mask (flags combined) */


#define DL_M_OPCODE     037                             /* opcode mask */
#define DL_M_UNIT       017                             /* unit mask */

#define DL_M_SPD        007                             /* S/P/D flags mask */
#define DL_M_CHEAD      003                             /* Cold Load Read head number mask */
#define DL_M_CSECT      077                             /* Cold Load Read sector number mask */
#define DL_M_FRETRY     017                             /* Set File Mask retry count mask */
#define DL_M_FMASK      017                             /* Set File Mask flags mask */


#define GET_OPCODE(c)   (CNTLR_OPCODE) (((c) >> DL_V_OPCODE) & DL_M_OPCODE)
#define GET_UNIT(c)     (((c) >> DL_V_UNIT)   & DL_M_UNIT)

#define GET_SPD(c)      (((c) >> DL_V_SPD)    & DL_M_SPD)
#define GET_CHEAD(c)    (((c) >> DL_V_CHEAD)  & DL_M_CHEAD)
#define GET_CSECT(c)    (((c) >> DL_V_CSECT)  & DL_M_CSECT)
#define GET_FRETRY(c)   (((c) >> DL_V_FRETRY) & DL_M_FRETRY)
#define GET_FMASK(c)    (((c) >> DL_V_FMASK)  & DL_M_FMASK)

#define DL_FDECR        (1 << DL_V_FDECR)
#define DL_FSPEN        (1 << DL_V_FSPEN)
#define DL_FCYLM        (1 << DL_V_FCYLM)
#define DL_FAUTSK       (1 << DL_V_FAUTSK)


/* Parameter accessors */

#define DL_V_HEAD       8                               /* bits 12- 8: head number */
#define DL_V_SECTOR     0                               /* bits  7- 0: sector number */

#define DL_M_HEAD       0017                            /* head number mask */
#define DL_M_SECTOR     0377                            /* sector number mask */

#define GET_HEAD(p)     (((p) >> DL_V_HEAD) & DL_M_HEAD)
#define GET_SECTOR(p)   (((p) >> DL_V_SECTOR) & DL_M_SECTOR)

#define SET_HEAD(c)     (uint16) (((c)->head & DL_M_HEAD) << DL_V_HEAD)
#define SET_SECTOR(c)   (uint16) (((c)->sector & DL_M_SECTOR) << DL_V_SECTOR)


/* 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 seeks).

   In simulation, we set up a table of drive properties and use the model ID as
   an index into the 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 additional "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.
*/

#define D7905_RH        2
#define D7905_FH        (D7905_HEADS - D7905_RH)

#define D7906_RH        2
#define D7906_FH        (D7906_HEADS - D7906_RH)

#define D7920_RH        D7920_HEADS
#define D7920_FH        (D7920_HEADS - D7920_RH)

#define D7925_RH        D7925_HEADS
#define D7925_FH        (D7925_HEADS - D7925_RH)

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


static const DRIVE_PROPERTIES drive_props [] = {
    { D7905_SECTS, D7905_HEADS, D7905_CYLS, D7905_WORDS, D7905_TYPE, D7905_RH, D7905_FH },
    { D7906_SECTS, D7906_HEADS, D7906_CYLS, D7906_WORDS, D7906_TYPE, D7906_RH, D7906_FH },
    { D7920_SECTS, D7920_HEADS, D7920_CYLS, D7920_WORDS, D7920_TYPE, D7920_RH, D7920_FH },
    { D7925_SECTS, D7925_HEADS, D7925_CYLS, D7925_WORDS, D7925_TYPE, D7925_RH, D7925_FH }
    };

#define PROPS_COUNT     (sizeof (drive_props) / sizeof (drive_props [0]))


/* Convert a CHS address to a block offset.

   A cylinder/head/sector address is converted into a linear block address that
   may be used to calculate a byte offset to pass to the file access routines.
   The conversion logic is:

     if Head < removable_heads_per_cylinder then
        tracks := Cylinder * removable_heads_per_cylinder + Head;
     else
        tracks := cylinders_per_drive * removable_heads_per_cylinder +
                  Cylinder * fixed_heads_per_cylinder + (Head - removable_heads_per_cylinder);

     block := tracks * sectors_per_track + Sector;

     byte_offset := block * words_per_sector * bytes_per_word;

   The byte offset is calculated in two steps to allow for future controller
   enhancements to support the CS/80 command set and its associated linear block
   addressing mode.
*/

#define TO_BLOCK(cylinder,head,sector,model) \
          (((head) < drive_props [model].remov_heads \
            ? (cylinder) * drive_props [model].remov_heads + (head) \
            : drive_props [model].cylinders * drive_props [model].remov_heads \
              + ((cylinder) * drive_props [model].fixed_heads + (head) - drive_props [model].remov_heads)) \
          * drive_props [model].sectors + (sector))

#define TO_OFFSET(block)    ((block) * DL_WPSEC * sizeof (uint16))


/* Estimate the current sector.

   The sector currently passing under the disc heads is estimated from the
   current simulator time (i.e., the count of instructions since startup) and
   the simulated disc rotation time.  The computation logic is:

     per_sector_time := word_transfer_time * words_per_sector + intersector_time;

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

#define GET_CURSEC(cvptr,uptr) \
          ((uint16) fmod (sim_gtime() / (double) ((cvptr->data_time * DL_WPSEC + cvptr->sector_time)), \
                          (double) drive_props [GET_MODEL (uptr->flags)].sectors))


/* Command properties table.

   The validity of each command for a specified controller type is checked
   against the command properties table when it is prepared.  The table also
   includes the count of inbound and outbound properties, the class of the
   command, and flags to indicate certain common actions that should be taken.
*/

typedef struct {
    uint32       params_in;                             /* count of input parameters */
    uint32       params_out;                            /* count of output parameters */
    CNTLR_CLASS  classification;                        /* command classification */
    t_bool       valid [type_count];                    /* per-type command validity */
    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 */
    } DS_PROPS;

typedef const DS_PROPS *PRPTR;

#define T   TRUE
#define F   FALSE

static const DS_PROPS cmd_props [] = {
/*   par par  opcode           valid for  clear unit  unit  unit  seek */
/*   in  out  classification   MAC  ICD   stat  field check acces wait */
    { 0,  0,  class_read,     { T,   T },   T,    F,    T,    T,    F },   /* 00 = cold load read */
    { 0,  0,  class_control,  { T,   T },   T,    T,    T,    T,    T },   /* 01 = recalibrate */
    { 2,  0,  class_control,  { T,   T },   T,    T,    T,    T,    F },   /* 02 = seek */
    { 0,  2,  class_status,   { T,   T },   F,    T,    F,    F,    F },   /* 03 = request status */
    { 0,  1,  class_status,   { T,   T },   T,    T,    T,    F,    F },   /* 04 = request sector address */
    { 0,  0,  class_read,     { T,   T },   T,    T,    T,    T,    T },   /* 05 = read */
    { 0,  0,  class_read,     { T,   T },   T,    T,    T,    T,    T },   /* 06 = read full sector */
    { 1,  0,  class_read,     { T,   T },   T,    T,    T,    T,    T },   /* 07 = verify */
    { 0,  0,  class_write,    { T,   T },   T,    T,    T,    T,    T },   /* 10 = write */
    { 0,  0,  class_write,    { T,   T },   T,    T,    T,    T,    T },   /* 11 = write full sector */
    { 0,  0,  class_control,  { T,   T },   T,    F,    F,    F,    F },   /* 12 = clear */
    { 0,  0,  class_write,    { T,   T },   T,    T,    T,    T,    T },   /* 13 = initialize */
    { 2,  0,  class_control,  { T,   T },   T,    F,    F,    F,    F },   /* 14 = address record */
    { 0,  7,  class_status,   { T,   F },   F,    F,    F,    F,    F },   /* 15 = request syndrome */
    { 1,  0,  class_read,     { T,   T },   T,    T,    T,    T,    T },   /* 16 = read with offset */
    { 0,  0,  class_control,  { T,   T },   T,    F,    F,    F,    F },   /* 17 = set file mask */
    { 0,  0,  class_invalid,  { F,   F },   T,    F,    F,    F,    F },   /* 20 = invalid */
    { 0,  0,  class_invalid,  { F,   F },   T,    F,    F,    F,    F },   /* 21 = invalid */
    { 0,  0,  class_read,     { T,   T },   T,    T,    T,    T,    T },   /* 22 = read without verify */
    { 1,  0,  class_status,   { T,   F },   T,    F,    F,    F,    F },   /* 23 = load TIO register */
    { 0,  2,  class_status,   { T,   T },   F,    F,    F,    F,    F },   /* 24 = request disc address */
    { 0,  0,  class_control,  { T,   T },   T,    F,    F,    F,    F },   /* 25 = end */
    { 0,  0,  class_control,  { T,   F },   T,    T,    T,    F,    F }    /* 26 = wakeup */
    };


/* Auxiliary unit indices */

typedef enum {
    controller = 0,                                     /* controller unit index */
    timer                                               /* command wait timer index */
    } AUX_INDEX;


/* Controller opcode names */

static const char invalid_name [] = "invalid";

static const char * const opcode_name [] = {
    "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 */
    };

/* Controller phase names */

static const char * const phase_name [] = {
    "start",
    "data",
    "end"
    };



/* Disc library local controller routines */

static t_bool start_seek      (CVPTR cvptr, UNIT *uptr, CNTLR_OPCODE next_opcode, CNTLR_PHASE next_phase);
static t_stat start_read      (CVPTR cvptr, UNIT *uptr);
static void   end_read        (CVPTR cvptr, UNIT *uptr);
static void   start_write     (CVPTR cvptr, UNIT *uptr);
static t_stat end_write       (CVPTR cvptr, UNIT *uptr);
static t_bool position_sector (CVPTR cvptr, UNIT *uptr, t_bool verify);
static void   next_sector     (CVPTR cvptr, UNIT *uptr);
static t_stat io_error        (CVPTR cvptr, UNIT *uptr, CNTLR_STATUS status);

/* Disc library local utility routines */

static void   set_address  (CVPTR cvptr, uint32    index);
static void   set_timer    (CVPTR cvptr, FLIP_FLOP action);
static uint16 drive_status (UNIT  *uptr);



/* Disc library global controller routines */


/* Prepare a command for execution.

   On entry, the first word of the controller buffer contains the command to
   prepare, the "cvptr" parameter points at the controller state variable
   structure, and the "units" parameter points at the first unit of the unit
   array.  For a MAC controller, the "unit limit" parameter indicates the last
   valid unit number, and the unit to use is taken from the unit field of the
   command word.  For an ICD controller, the parameter indicates the number
   of the unit to use directly.

   If a valid command was prepared for execution, the routine returns TRUE and
   sets the controller state to "busy."  If the command is illegal, the routine
   returns FALSE and sets the controller state to "waiting."  In the latter
   case, the controller status will indicate the reason for the rejection.

   The opcode and unit number (for MAC controllers) are obtained from the buffer
   and checked for legality.  If either is illegal, the controller status is set
   appropriately, and the routine returns FALSE.

   For a valid command and an available unit, the controller's opcode field is
   set from the buffer, the length field is set to the number of inbound
   parameter words expected, and the index field is set to 1 to point at the
   first parameter entry in the buffer.
*/

t_bool dl_prepare_command (CVPTR cvptr, UNIT *units, uint32 unit_limit)
{
uint32 unit;
PRPTR props;
CNTLR_OPCODE opcode;

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

opcode = GET_OPCODE (cvptr->buffer [0]);                /* get the opcode from the command */

if (opcode > Last_Opcode)                               /* is the opcode invalid? */
    props = &cmd_props [Invalid_Opcode];                /* undefined commands clear prior status */
else                                                    /* the opcode is potentially valid */
    props = &cmd_props [opcode];                        /* get the command properties */

if (cvptr->type == MAC)                                 /* is this a MAC controller? */
    if (props->unit_field)                              /* is the unit field defined for this command? */
        unit = GET_UNIT (cvptr->buffer [0]);            /* get the unit from the command */
    else                                                /* no unit specified in the command */
        unit = 0;                                       /*   so the unit is always unit 0 */

else                                                    /* an ICD controller */
    unit = unit_limit;                                  /*   uses the supplied unit number */

if (props->clear_status) {                              /* clear the prior controller status */
    cvptr->status = normal_completion;                  /*   if indicated for this command */
    cvptr->spd_unit = SET_S1UNIT (unit);                /* save the unit number for status requests */
    }

if (cvptr->type <= last_type                            /* is the controller type legal, */
  && props->valid [cvptr->type])                        /*   and the opcode defined for this controller? */
    if (props->unit_check && unit > DL_MAXUNIT)         /* if the unit number is checked and is illegal, */
        dl_end_command (cvptr, unit_unavailable);       /*   end with a unit unavailable error */

    else {
        cvptr->state = cntlr_busy;                      /* legal unit, so controller is now busy */
        cvptr->opcode = opcode;                         /* save the controller opcode */
        cvptr->length = props->params_in;               /* set the inbound parameter count */
        cvptr->index = 1;                               /* point at the first parameter element (if any) */

        if (cvptr->type == MAC && cvptr->length) {      /* is this a MAC controller with inbound parameters? */
            cvptr->aux [controller].OP = opcode;        /* save the opcode */
            cvptr->aux [controller].PHASE = data_phase; /*   and set the phase for parameter pickup */
            set_timer (cvptr, SET);                     /* start the timer to wait for the first parameter */
            }

        return TRUE;                                    /* the command is now prepared for execution */
        }

else                                                    /* the opcode is undefined */
    dl_end_command (cvptr, illegal_opcode);             /*   so set bad opcode status */

return FALSE;                                           /* the preparation has failed */
}


/* Start a command.

   On entry, the controller's opcode field contains the command to start, and
   the buffer contains the command word in element 0 and the parameters required
   by the command, if any, beginning in element 1.  The call parameters are the
   same as those supplied to the "prepare command" routine.

   If the command was started successfully, the routine returns a pointer to the
   unit to be activated and sets that unit's "wait" field to the activation
   time.  The caller should activate the unit upon return to complete or
   continue command processing.  If the command did not start, the routine
   returns NULL.

   If a seek is in progress on a drive when a command accessing that drive is
   started, the unit pointer is returned but the unit's "wait" field is set to
   zero.  In this case, the unit must not be activated (as it already is).
   Instead, the unit's opcode and phase fields will have been set to start the
   command automatically when the seek completes.

   For commands that return status from the controller, the buffer will contain
   the returned value(s), the buffer index will be zero, and the buffer length
   will be set to the number of words returned in the buffer.  These words must
   be returned to the CPU via the interface.


   Implementation notes:

    1. A command must have been prepared by calling dl_prepare_command first.
       After preparation, the controller's opcode will be valid, and the unit
       number field will be legal (but not necessarily valid) for those commands
       that check the unit.

       Unit numbers 0-7 represent valid drive addresses.  However, the MAC
       controller firmware allows access to unit numbers 8-10 without causing a
       Unit Unavailable error.  Instead, the controller reports these units as
       permanently offline.

    2. Commands that check for a valid unit do some processing before failing
       with a Status-2 (not ready) error if the unit is invalid.  For example,
       the Seek command accepts its parameters from the CPU and sets the CHS
       values into the controller before failing.

    3. In hardware, read, write, and recalibrate commands wait in an internal
       loop for a pending seek completion and clear the resulting Attention
       status before executing.  In simulation, we change a seeking drive unit's
       opcode and phase fields from seek completion to the start of the next
       command.  This eliminates the setting of the Attention status and begins
       command execution automatically when the seek completes.

       If the seek completed between the command preparation and start,
       Attention will have been set.  If the unit is idle on entry, we clear the
       Attention status unilaterally (it doesn't matter whether or not it was
       set; Attention always is clear when commands start).

    4. The Seek and Cold Load Read commands do not check for a seek or
       recalibrate in progress.  If the heads are moving, the drive will reject
       a seek command with a Seek Check error.  The firmware does not test
       explicitly for Access Not Ready before executing the command, so 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.

       A Cold Load Read command issues a seek to cylinder 0 and then begins a
       read, which first waits for seek completion.  The Seek Check error will
       abort the command at this point with Status-2 Error status.

       In simulation, a Seek command allows the seek in progress to complete
       normally, whereas a Cold Load Read command modifies the unit command
       and phase from the end phase of Seek or Recalibrate to the start
       phase of Read, which will catch the Seek Check error as in hardware.

    5. The Cold Load Read command checks if the drive is ready before setting
       the file mask.  Therefore, we normally defer setting the file mask until
       the unit service is called.  However, if a seek is in progress, then the
       drive must be ready, so we set the file mask here.

    6. ECC is not simulated, so the Request Syndrome command always returns zero
       values for the displacement and patterns.

    7. The Request Status, Request Sector Address, and Wakeup commands reference
       drive units but are scheduled on the controller unit because they may be
       issued while a drive is processing a seek.

    8. The activation time is set to the intersector time (latency) for read and
       write commands, and to the controller processing time for all others.
       The read/write start time cannot be shorter than 20 instructions, or
       DVR32 will be unable to start DCPC in time to avoid an over/underrun.
*/

UNIT *dl_start_command (CVPTR cvptr, UNIT *units, uint32 unit_limit)
{
UNIT *uptr, *rptr;
uint32 unit;
PRPTR props;
t_bool is_seeking = FALSE;

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

if (cvptr->type == MAC) {                               /* is this a MAC controller? */
    if (props->unit_field)                              /* is the unit field defined for this command? */
        unit = GET_UNIT (cvptr->buffer [0]);            /* get the unit number from the command */
    else                                                /* no unit is specified in the command */
        unit = 0;                                       /*   so the unit number defaults to 0 */

    if (unit > unit_limit)                              /* if the unit number is invalid, */
        uptr = NULL;                                    /*   it does not correspond to a unit */
    else if (props->unit_access)                        /* if the command accesses a drive, */
        uptr = units + unit;                            /*   get the address of the unit */
    else                                                /* the command accesses the controller only */
        uptr = cvptr->aux + controller;                 /*   so use the controller unit */
    }

else {                                                  /* for an ICD controller, */
    unit = 0;                                           /*   the unit value is ignored */
    uptr = units + unit_limit;                          /*     and we use the indicated unit */
    }

if (props->unit_check && !uptr                                  /* if the unit number is checked and is invalid */
  || props->seek_wait && (drive_status (uptr) & DL_S2STOPS)) {  /*   or if we're waiting for an offline drive */
    dl_end_command (cvptr, status_2_error);                     /*     then the command ends with a Status-2 error */
    uptr = NULL;                                                /* prevent the command from starting */
    }

else if (uptr) {                                        /* otherwise, we have a valid unit */
    uptr->wait = cvptr->cmd_time;                       /* most commands use the command delay */

    if (props->unit_access) {                           /* does the command access the unit? */
        is_seeking = sim_is_active (uptr);              /* see if the unit is busy */

        if (is_seeking)                                 /* if a seek is in progress, */
            uptr->wait = 0;                             /*   set for no unit activation */

        else {                                          /* otherwise, the unit is idle */
            uptr->STAT &= ~DL_S2ATN;                    /* clear the drive Attention status */

             if (props->classification == class_read    /* if a read command */
              || props->classification == class_write)  /*   or a write command */
                uptr->wait = cvptr->sector_time;        /*     schedule the sector start latency */
            }
        }
    }

cvptr->index = 0;                                       /* reset the buffer index */
cvptr->length = props->params_out;                      /* set the count of outbound parameters */
cvptr->eod = CLEAR;                                     /* clear the end of data flag */


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

    case Cold_Load_Read:
        cvptr->cylinder = 0;                            /* set the cylinder address to 0 */
        cvptr->head = GET_CHEAD (cvptr->buffer [0]);    /* set the head */
        cvptr->sector = GET_CSECT (cvptr->buffer [0]);  /*   and sector from the command */

        if (is_seeking) {                               /* if a seek is in progress, */
            uptr->STAT |= DL_S2SC;                      /*   a Seek Check occurs */
            cvptr->file_mask = DL_FSPEN;                /* enable sparing */
            uptr->OP = Read;                            /* start the read on the seek completion  */
            uptr->PHASE = start_phase;                  /*   and reset the command phase */
            return uptr;                                /*     to allow the seek to complete normally */
            }

        else                                            /* the drive is not seeking */
            uptr->wait = cvptr->cmd_time;               /* the command starts with a seek, not a read */

        break;


    case Seek:
        cvptr->cylinder = cvptr->buffer [1];            /* get the supplied cylinder */
        cvptr->head = GET_HEAD (cvptr->buffer [2]);     /*   and head */
        cvptr->sector = GET_SECTOR (cvptr->buffer [2]); /*     and sector addresses */

        if (is_seeking) {                               /* if a seek is in progress, */
            uptr->STAT |= DL_S2SC;                      /*   a Seek Check occurs */
            dl_idle_controller (cvptr);                 /* return the controller to the idle condition */
            return uptr;                                /*   to allow the seek to complete normally */
            }

        break;


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

        if (cvptr->type == MAC)                         /* is this a MAC controller? */
            if (unit > unit_limit)                      /* if the unit number is invalid */
                rptr = NULL;                            /*   it does not correspond to a unit */
            else                                        /* otherwise, the unit is valid */
                rptr = &units [unit];                   /*   so get the address of the referenced unit */
        else                                            /* if not a MAC controller */
            rptr = uptr;                                /*   then the referenced unit is the current unit */

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

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

        cvptr->spd_unit = SET_S1UNIT (unit);            /* save the unit number */

        if (unit > DL_MAXUNIT)                          /* if the unit number is illegal, */
            cvptr->status = unit_unavailable;           /*   the next status will be Unit Unavailable */
        else                                            /* a legal unit */
            cvptr->status = normal_completion;          /*   clears the controller status */

        break;


    case Request_Disc_Address:
        set_address (cvptr, 0);                         /* return the CHS values in buffer 0-1 */
        break;


    case Request_Sector_Address:
        if (unit > unit_limit)                              /* if the unit number is invalid */
            rptr = NULL;                                    /*   it does not correspond to a unit */
        else                                                /* otherwise, the unit is valid */
            rptr = &units [unit];                           /*   so get the address of the referenced unit */

        if (drive_status (rptr) & DL_S2NR)                  /* if the drive is not ready, */
            dl_end_command (cvptr, status_2_error);         /*   terminate with not ready status */
        else                                                /* otherwise, the drive is ready */
            cvptr->buffer [0] = GET_CURSEC (cvptr, rptr);   /*   so calculate the current sector address */
        break;


    case Request_Syndrome:
        cvptr->buffer [0] = (uint16) (cvptr->spd_unit           /* return the Status-1 value */
                              | SET_S1STAT (cvptr->status));    /*   in buffer 0 */

        set_address (cvptr, 1);                         /* return the CHS values in buffer 1-2 */

        cvptr->buffer [3] = 0;                          /* the displacement is always zero */
        cvptr->buffer [4] = 0;                          /* the syndrome is always zero */
        cvptr->buffer [5] = 0;
        cvptr->buffer [6] = 0;
        break;


    case Address_Record:
        cvptr->cylinder = cvptr->buffer [1];            /* get the supplied cylinder */
        cvptr->head = GET_HEAD (cvptr->buffer [2]);     /*   and head */
        cvptr->sector = GET_SECTOR (cvptr->buffer [2]); /*     and sector addresses */
        cvptr->eoc = CLEAR;                             /* clear the end-of-cylinder flag */
        break;


    case Set_File_Mask:
        cvptr->file_mask = GET_FMASK (cvptr->buffer [0]);   /* get the supplied file mask */

        if (cvptr->type == MAC)                             /* if this is a MAC controller, */
            cvptr->retry = GET_FRETRY (cvptr->buffer [0]);  /*   the retry count is supplied too */
        break;


    case Initialize:
        if (uptr)                                       /* if the unit is valid, */
            cvptr->spd_unit |=                          /*   merge the SPD flags */
              SET_S1SPD (GET_SPD (cvptr->buffer [0]));  /*     from the command word */
        break;


    case Verify:
        cvptr->verify_count = cvptr->buffer [1];        /* get the supplied sector count */
        break;


    default:                                            /* the remaining commands */
        break;                                          /*   are handled by the service routines */
    }


if (uptr) {                                             /* if the command accesses a valid unit */
    uptr->OP = cvptr->opcode;                           /*   save the opcode in the unit */

    if (cvptr->length)                                  /* if the command has outbound parameters, */
        uptr->PHASE = data_phase;                       /*   set up the data phase for the transfer */
    else                                                /* if there are no parameters, */
        uptr->PHASE = start_phase;                      /*   set up the command phase for execution */

    return uptr;                                        /* return a pointer to the scheduled unit */
    }

else
    return NULL;                                        /* the command did not start */
}


/* Complete a command.

   The current command is completed with the indicated status.  The command
   result status is set, the controller enters the command wait state, and the
   CPU timer is restarted.
*/

void dl_end_command (CVPTR cvptr, CNTLR_STATUS status)
{
cvptr->status = status;                                 /* set the command result status */
cvptr->state = cntlr_wait;                              /* set the controller state to waiting */
set_timer (cvptr, SET);                                 /* start the command wait timer */
return;
}


/* Poll the drives for Attention status.

   If interrupts are enabled on the interface, this routine is called to check
   if any drive is requesting attention.  The routine returns TRUE if a drive is
   requesting attention and FALSE if not.

   Starting with the last unit requesting 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 TRUE to indicate 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 FALSE to indicate
   that no interrupt should be generated.
*/

t_bool dl_poll_drives (CVPTR cvptr, UNIT *units, uint32 unit_limit)
{
uint32 unit;

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

    if (units [cvptr->poll_unit].STAT & DL_S2ATN) {         /* if the unit is requesting attention, */
        units [cvptr->poll_unit].STAT &= ~DL_S2ATN;         /*   clear the Attention status */
        cvptr->spd_unit = SET_S1UNIT (cvptr->poll_unit);    /* set the controller's unit number */
        cvptr->status = drive_attention;                    /*   and status */
        cvptr->state = cntlr_wait;                          /*     and wait for a command */
        return TRUE;                                        /* tell the caller to interrupt */
        }
    }

return FALSE;                                               /* no requests, so do not generate an interrupt */
}


/* Service the disc drive unit.

   The unit service routine is called to execute scheduled controller commands
   for the specified unit.  The actions to be taken depend on the current state
   of the controller and the unit.

   In addition to the controller state variables supplied in the call, the
   service routine accesses these six variables in the UNIT structure:

     wait       -- the current service activation time
     pos        -- the current byte offset into the disc image file
     u3 (CYL)   -- the current drive cylinder
     u4 (STAT)  -- the drive status (Status-2)
     u5 (OP)    -- the drive operation in process
     u6 (PHASE) -- the current operation phase

   The activation time is set non-zero if the service should be rescheduled.
   The caller is responsible upon return for activating the unit.  The file
   offset indicates the byte position in the disc image file for the next read
   or write operation.

   The drive cylinder gives the current location of the head positioner.  This
   may differ from the cylinder value in the controller if the Address Record
   command has been used.  The drive status maintains various per-drive
   conditions (e.g., the state of the read-only and format switches, drive
   ready, first status).  The operation in process and operation phase define
   the action to be taken by this service routine.

   Initially, the operation in process is set to the opcode field of the command
   when it is started.  However, the operation in process may change during
   execution (the controller opcode never does).  This is to aid code reuse in
   the service routine.  For example, a Cold Load Read command is changed to a
   Read command once the seek portion is complete, and a Read Without Verify
   command is changed to a normal Read command after a track boundary is
   crossed.

   The operation phase provides different substates for those commands that
   transfer data or that have different starting and ending actions.  Three
   phases are defined: start, data, and end.  Commands that do not transfer data
   to or from the CPU interface do not have data phases, and commands that
   complete upon first service do not have end phases.  The service routine
   validates phase assignments and returns SCPE_IERR (Internal Error) if entry
   is made with an illegal operation phase or a phase that is not valid for a
   given operation.

   An operation in the data phase is in the process of transferring data between
   the CPU and sector buffer.  Because this process is interface-specific, the
   service routine does nothing (other than validate) in this phase.  It is up
   to the caller to transition from the data phase to the end phase when the
   transfer is complete.

   If an operation is completed, or an error has occurred, the controller state
   on return will be either idle or waiting, instead of busy.  The caller should
   check the controller status to determine if normal completion or error
   recovery is appropriate.

   If the command is continuing, the service activation time will be set
   appropriately.  The caller should then call sim_activate to schedule the next
   service and clear the "wait" field in preparation for the next service call.


   Implementation notes:

    1. The Cold Load Read and Seek commands check only the drive's Not Ready
       status because seeking clears a Seek Check.  The other commands that
       access the unit (e.g., Read and Write) have already checked in the
       command start routine for Not Ready, Seek Check, or Fault status and
       terminated with a Status-2 error.

    2. Several commands (e.g., Set File Mask, Address Record) are executed
       completely within the dl_start_command routine, so all we do here is
       finish the command with the expected status.  The service routine is
       called only to provide the proper command execution delay.

    3. If a host file system error occurs, the service routine returns SCPE_IERR
       to stop simulation.  If simulation is resumed, the controller will behave
       as though an uncorrectable data error had occurred.
*/

t_stat dl_service_drive (CVPTR cvptr, UNIT *uptr)
{
t_stat result = SCPE_OK;
const CNTLR_OPCODE opcode = (CNTLR_OPCODE) uptr->OP;

switch ((CNTLR_PHASE) uptr->PHASE) {                    /* dispatch the phase */

    case start_phase:
        switch (opcode) {                               /* dispatch the current operation */

            case Recalibrate:
            case Seek:
                if (start_seek (cvptr, uptr, opcode, end_phase)     /* start the seek; if it succeeded, */
                  && (cvptr->type == MAC))                          /*   and this a MAC controller, */
                    dl_idle_controller (cvptr);                     /*     then go idle until it completes */
                break;


            case Cold_Load_Read:
                if (start_seek (cvptr, uptr, Read, start_phase))    /* start the seek; did it succeed? */
                    cvptr->file_mask = DL_FSPEN;                    /* set sparing enabled now */
                break;


            case Read:
            case Read_With_Offset:
            case Read_Without_Verify:
                cvptr->length = DL_WPSEC;               /* transfer just the data */
                result = start_read (cvptr, uptr);      /* start the sector read */
                break;


            case Read_Full_Sector:
                cvptr->length = DL_WPFSEC;              /* transfer the header/data/trailer */
                result = start_read (cvptr, uptr);      /* start the sector read */
                break;


            case Verify:
                cvptr->length = 0;                                  /* no data transfer needed */
                result = start_read (cvptr, uptr);                  /* start the sector read */

                if (uptr->PHASE == data_phase) {                    /* did the read start successfully? */
                    uptr->PHASE = end_phase;                        /* skip the data phase */
                    uptr->wait = cvptr->sector_time                 /* reschedule for the intersector time */
                                   + cvptr->data_time * DL_WPSEC;   /*   plus the data read time */
                    }
                break;


            case Write:
            case Initialize:
                cvptr->length = DL_WPSEC;               /* transfer just the data */
                start_write (cvptr, uptr);              /* start the sector write */
                break;


            case Write_Full_Sector:
                cvptr->length = DL_WPFSEC;              /* transfer the header/data/trailer */
                start_write (cvptr, uptr);              /* start the sector write */
                break;


            case Request_Status:
            case Request_Sector_Address:
            case Clear:
            case Address_Record:
            case Request_Syndrome:
            case Set_File_Mask:
            case Load_TIO_Register:
            case Request_Disc_Address:
            case End:
            case Wakeup:
                dl_service_controller (cvptr, uptr);    /* the controller service handles these */
                break;


            default:                                    /* we were entered with an invalid state */
                result = SCPE_IERR;                     /* return an internal (programming) error */
                break;
            }                                           /* end of operation dispatch */
        break;                                          /* end of start phase handlers */


    case data_phase:
        switch (opcode) {                               /* dispatch the current operation */
            case Read:
            case Read_Full_Sector:
            case Read_With_Offset:
            case Read_Without_Verify:
            case Write:
            case Write_Full_Sector:
            case Initialize:
                break;                                  /* data transfers are handled by the caller */


            default:                                    /* entered with an invalid state */
                result = SCPE_IERR;                     /* return an internal (programming) error */
                break;
            }                                           /* end of operation dispatch */
        break;                                          /* end of data phase handlers */


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

            case Recalibrate:
            case Seek:
                if (cvptr->type == ICD)                         /* is this an ICD controller? */
                    dl_end_command (cvptr, drive_attention);    /* seeks end with Drive Attention status */
                else                                            /* if not an ICD controller, */
                    uptr->STAT |= DL_S2ATN;                     /*   set Attention in the unit status */
                break;


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


            case Read_Without_Verify:
                if (cvptr->sector == 0)                 /* have we reached the end of the track? */
                    uptr->OP = Read;                    /* begin verifying the next time */

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


            case Verify:
                cvptr->verify_count =                   /* decrement the count */
                  (cvptr->verify_count - 1) & D16_MASK; /*   modulo 65536 */

                if (cvptr->verify_count == 0)           /* are there more sectors to verify? */
                    cvptr->eod = SET;                   /* no, so terminate the command cleanly */

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


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


            case Request_Status:
            case Request_Sector_Address:
            case Request_Disc_Address:
                dl_service_controller (cvptr, uptr);    /* the controller service handles these */
                break;


            default:                                    /* we were entered with an invalid state */
                result = SCPE_IERR;                     /* return an internal (programming) error */
                break;
            }                                           /* end of operation dispatch */
        break;                                          /* end of end phase handlers */
    }                                                   /* end of phase dispatch */

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


/* Service the controller unit.

   The controller service routine is called to execute scheduled controller
   commands that do not access drive units.  It is also called to obtain command
   parameters from the interface and to return command result values to the
   interface.  The actions to be taken depend on the current state of the
   controller.

   Controller commands are scheduled on a separate unit to allow concurrent
   processing while seeks are in progress.  For example, a seek may be started
   on unit 0.  While the seek is in progress, the CPU may request status from
   the controller.  In between returning the first and second status words to
   the CPU, the seek may complete.  Separating the controller unit allows seek
   completion to be handled while the controller is "busy" waiting for the CPU
   to indicate that it is ready for the second word.

   For ICD controllers, the controller unit is not used, and all commands are
   scheduled on the drive unit.  This is possible because ICD controllers always
   wait for seeks to complete before executing additional commands.  To reduce
   code duplication, however, the drive unit service calls the controller
   service directly to handle controller commands.

   The service routine validates phase assignments and returns SCPE_IERR
   (Internal Error) if entry is made with an illegal operation phase or a phase
   that is not valid for a given operation.

   Implementation notes:

    1. While the interface simulator is responsible for data phase transfers,
       the controller service routine is responsible for (re)starting and
       stopping the command wait timer for each parameter sent to and received
       from the interface.
*/

t_stat dl_service_controller (CVPTR cvptr, UNIT *uptr)
{
t_stat result = SCPE_OK;
const CNTLR_OPCODE opcode = (CNTLR_OPCODE) uptr->OP;

switch ((CNTLR_PHASE) uptr->PHASE) {                    /* dispatch the phase */

    case start_phase:
    case end_phase:
        switch (opcode) {                               /* dispatch the current operation */
            case Request_Status:
                dl_end_command (cvptr, cvptr->status);  /* the command completes with no status change */
                break;


            case Clear:
                dl_clear_controller (cvptr, uptr, soft_clear);  /* clear the controller */
                dl_end_command (cvptr, normal_completion);      /* the command is complete */
                break;


            case Request_Sector_Address:
            case Address_Record:
            case Request_Syndrome:
            case Set_File_Mask:
            case Load_TIO_Register:
            case Request_Disc_Address:
                dl_end_command (cvptr, normal_completion);      /* the command is complete */
                break;


            case End:
                dl_idle_controller (cvptr);             /* the command completes with the controller idle */
                break;


            case Wakeup:
                dl_end_command (cvptr, unit_available); /* the command completes with Unit Available status */
                break;


            default:                                    /* we were entered with an invalid state */
                result = SCPE_IERR;                     /* return an internal (programming) error */
                break;
            }                                           /* end of operation dispatch */
        break;                                          /* end of start and end phase handlers */


    case data_phase:
        switch (opcode) {                               /* dispatch the current operation */

            case Seek:
            case Verify:
            case Address_Record:
            case Read_With_Offset:
            case Load_TIO_Register:
                if (cvptr->length > 1)                  /* at least one more parameter to input? */
                    set_timer (cvptr, SET);             /* restart the timer for the next parameter */
                else                                    /* this is the last one */
                    set_timer (cvptr, CLEAR);           /*   so stop the command wait timer */
                break;


            case Request_Status:
            case Request_Sector_Address:
            case Request_Syndrome:
            case Request_Disc_Address:
                if (cvptr->length > 0)                  /* at least one more to parameter output? */
                    set_timer (cvptr, SET);             /* restart the timer for the next parameter */
                else                                    /* this is the last one */
                    set_timer (cvptr, CLEAR);           /*   so stop the command wait timer */
                break;


            default:                                    /* we were entered with an invalid state */
                result = SCPE_IERR;                     /* return an internal (programming) error */
                break;
            }                                           /* end of operation dispatch */
        break;                                          /* end of data phase handlers */
    }                                                   /* end of phase dispatch */

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


/* Service the command wait timer unit.

   The command wait timer service routine is called if the command wait timer
   expires.  This indicates that the CPU did not respond to a parameter transfer
   or did not issue a new command within the ~1.8 second timeout period.  The
   timer is used with the MAC controller to ensure that a hung CPU does not tie
   up the controller, preventing it from servicing other CPUs or drives.  ICD
   controllers do not use the command wait timer; they will wait forever, as
   each controller is dedicated to a single interface.

   When a timeout occurs, the controller unit is cancelled in case the cause was
   a parameter timeout.  Then the file mask is reset, and the controller is
   idled.

   The interface is responsible for polling for a new command and for drive
   attention when a timeout occurs.

   Implementation notes:

    1. Only the controller unit may be active when the command wait timer
       expires.  A unit is never active because the timer is cancelled when
       commands are executing and is restarted after the command completes.
*/

t_stat dl_service_timer (CVPTR cvptr, UNIT *uptr)
{
sim_cancel (cvptr->aux);                                /* cancel any controller activation */

dl_idle_controller (cvptr);                             /* idle the controller */
cvptr->file_mask = 0;                                   /* clear the file mask */

return SCPE_OK;
}


/* Clear the controller.

   The controller connected to the specified unit is cleared as directed.  A MAC
   controller is connected to several units, so the unit is used to find the
   associated device and thereby the unit array.  An ICD controller is connected
   only to the specified unit.

   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 ENABLE jumper is not installed (which is the usual case).  The third
   condition also causes a firmware restart but with the PWRON flag clear.  The
   last condition 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)
    - issue a Controller Preset to clear all connected drives
    - clear the clock offset
    - clear the file mask
    - 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

   In simulation, a hard clear occurs when a RESET -P or RESET command is
   issued, or a programmed CLC 0 instruction is executed.  A soft clear occurs
   when a programmed Clear command is started.  A timeout clear occurs when the
   command wait timer unit is serviced, but this action is handled in the timer
   unit service.

   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 or timeout 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. In simulation, a Controller Preset only resets the specified status bits,
       as the remainder of the hardware actions are not implemented.
*/

t_stat dl_clear_controller (CVPTR cvptr, UNIT *uptr, CNTLR_CLEAR clear_type)
{
uint32 unit, unit_count;
DEVICE *dptr = NULL;

if (clear_type == hard_clear) {                         /* is this a hard clear? */
    dl_idle_controller (cvptr);                         /* idle the controller */
    cvptr->file_mask = 0;                               /* clear the file mask */
    cvptr->poll_unit = 0;                               /* clear the last unit polled */
    }

if (cvptr->type == ICD)                                 /* is this an ICD controller? */
    unit_count = 1;                                     /* there is only one unit per controller */

else {                                                  /* a MAC controller clears all units */
    dptr = find_dev_from_unit (uptr);                   /* find the associated device */

    if (dptr == NULL)                                   /* the device doesn't exist?!? */
        return SCPE_IERR;                               /* this is an impossible condition! */
    else                                                /* the device was found */
        unit_count = dptr->numunits;                    /*   so get the number of units */
    }

for (unit = 0; unit < unit_count; unit++) {             /* loop through the unit(s) */
    if (dptr)                                           /* pick up the unit from the device? */
        uptr = dptr->units + unit;                      /* yes, so get the next unit */

    if (!(uptr->flags & UNIT_DIS)) {                    /* is the unit enabled? */
        if (clear_type == hard_clear                    /* a hard clear cancels */
          && uptr->OP != Seek                           /*   only if not seeking */
          && uptr->OP != Recalibrate)                   /*     or recalibrating */
            sim_cancel (uptr);                          /* cancel the service */

        uptr->STAT &= ~DL_S2CPS;                        /* do "Controller Preset" for the unit */
        }
    }

return SCPE_OK;
}


/* 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).
*/

void dl_idle_controller (CVPTR cvptr)
{
cvptr->state = cntlr_idle;                              /* idle the controller */
cvptr->status = normal_completion;                      /* the Poll Loop clears the status */

set_timer (cvptr, CLEAR);                               /* stop the command wait timer */
return;
}



/* 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 simulation, the unit must be attached before the heads may be unloaded or
   loaded.  As the heads should be automatically loaded when a unit is attached
   and unloaded when a unit is detached, this routine must be called after
   attaching and before detaching.


   Implementation notes:

    1. The drive sets its Attention status bit when the heads load or unload.
       However, the ICD controller reports Attention only for head unloading.

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

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

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;                                  /* return "Unit not attached" if not */

else if (!(sim_switches & SIM_SW_REST))                 /* modify the flags only if not restoring */
    if (load) {                                         /* are we loading the heads? */
        uptr->flags = uptr->flags & ~UNIT_UNLOAD;       /* clear the unload flag */
        uptr->STAT = DL_S2FS;                           /*   and set First Status */

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

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

return SCPE_OK;
}



/* Disc library global utility routines */


/* Classify the current controller opcode.

   The controller opcode is classified as a read, write, control, or status
   command, and the classification is returned to the caller.  If the opcode is
   illegal or undefined for the indicated controller, the classification is
   marked as invalid.
*/

CNTLR_CLASS dl_classify (CNTLR_VARS cntlr)
{
if (cntlr.type <= last_type                             /* if the controller type is legal */
  && cntlr.opcode <= Last_Opcode                        /*   and the opcode is legal */
  && cmd_props [cntlr.opcode].valid [cntlr.type])       /*   and is defined for this controller, */
    return cmd_props [cntlr.opcode].classification;     /*     then return the command classification */
else                                                    /* the type or opcode is illegal */
    return class_invalid;                               /*   so return an invalid classification */
}


/* 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_type                             /* 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                                                    /* the type or opcode is illegal, */
    return invalid_name;                                /*   so return an error indication */
}


/* Return the name of a command phase.

   A string representing the supplied phase is returned to the caller.  If the
   phase is illegal, the string "invalid" is returned.
*/

const char *dl_phase_name (CNTLR_PHASE phase)
{
if (phase <= last_phase)                                /* if the phase is legal, */
    return phase_name [phase];                          /*   return the phase name */
else                                                    /* the phase is illegal, */
    return invalid_name;                                /*   so return an error indication */
}



/* Disc library global VM 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.

   If the drive is set to autosize, the size of the image file is compared to
   the table of drive capacities to determine which model of drive was used to
   create it.  If the image file is new, then the previous drive model is
   retained.
*/

t_stat dl_attach (CVPTR cvptr, UNIT *uptr, CONST char *cptr)
{
uint32 id, size;
t_stat result;

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

if (result != SCPE_OK)                                      /* did the attach fail? */
    return result;                                          /* yes, so return the error status */

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

if (uptr->flags & UNIT_AUTO) {                              /* is autosizing enabled? */
    size = sim_fsize (uptr->fileref) / sizeof (uint16);     /* get the file size in words */

    if (size > 0)                                           /* a new file retains the current drive model */
        for (id = 0; id < PROPS_COUNT; id++)                /* find the best fit to the drive models */
            if (size <= drive_props [id].words              /* if the file size fits the drive capacity */
              || id == PROPS_COUNT - 1) {                   /*   or this is the largest available drive */
                uptr->capac = drive_props [id].words;       /*     then set the capacity */
                uptr->flags = (uptr->flags & ~UNIT_MODEL)   /*       and the model */
                  | SET_MODEL (id);
                break;
                }
    }

return SCPE_OK;                                             /* the unit was successfully attached */
}


/* 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)
{
dl_load_unload (cvptr, uptr, FALSE);                    /* unload the heads if attached */
return detach_unit (uptr);                              /*   and detach the unit */
}


/* Set the drive model.

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

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

if (value != UNIT_AUTO)                                     /* if we are not autosizing */
    uptr->capac = drive_props [GET_MODEL (value)].words;    /*   set the capacity to the new value */

return SCPE_OK;
}



/* Disc library local controller routines */


/* Start a read operation on the current sector.

   The current sector indicated 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.

   On entry, the end-of-data flag is checked.  If it is set, the current read 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 operation phase unchanged 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 cylinder, 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
   file read did not return a full sector, the remainder of the buffer is padded
   with zeros.  If the image read failed with a file system error, SCPE_IOERR is
   returned from the service routine to cause a simulation stop; resumption is
   handled as an Uncorrectable Data Error.

   If the image was read correctly, the next sector address is updated, the
   operation phase is set for the data transfer, and the index of the first word
   to transfer is set.


   Implementation notes:

    1. The length of the transfer required (cvptr->length) must be set before
       entry.

    2. Entry while executing a Read Without Verify or Read Full Sector command
       inhibits address verification.  The unit opcode is tested instead of the
       controller opcode because a Read Without Verify is changed to a Read to
       begin verifying after a track switch occurs.
*/

static t_stat start_read (CVPTR cvptr, UNIT *uptr)
{
uint32 count, offset;
t_bool verify;
const CNTLR_OPCODE opcode = (CNTLR_OPCODE) uptr->OP;

if (cvptr->eod == SET) {                                /* is the end of data indicated? */
    dl_end_command (cvptr, normal_completion);          /* complete the command */
    return SCPE_OK;
    }

if (opcode == Read_Full_Sector) {                       /* are we starting a Read Full Sector command? */
    if (cvptr->type == ICD)                             /* is this an ICD controller? */
        cvptr->buffer [0] = 0100377;                    /* ICD does not support ECC */
    else
        cvptr->buffer [0] = 0100376;                    /* MAC does support ECC */

    set_address (cvptr, 1);                             /* set the current address into buffer 1-2 */
    offset = 3;                                         /* start the data after the header */
    verify = FALSE;                                     /* set for no address verification */
    }

else {                                                  /* it's another read command */
    offset = 0;                                         /* data starts at the beginning */
    verify = (opcode != Read_Without_Verify);           /* set for address verification unless it's a RWV */
    }

if (! position_sector (cvptr, uptr, verify))            /* position the sector */
    return SCPE_OK;                                     /* a seek is in progress or an error occurred */

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

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 */

if (ferror (uptr->fileref))                                     /* did a host file system error occur? */
    return io_error (cvptr, uptr, uncorrectable_data_error);    /* set up the data error status and stop the simulation */

next_sector (cvptr, uptr);                              /* address the next sector */

uptr->PHASE = data_phase;                               /* set up the data transfer phase */
cvptr->index = 0;                                       /* reset the data index */

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


/* Finish a read operation on the current sector.

   On entry, the end-of-data flag is checked.  If it is set, the current read is
   completed.  Otherwise, the command phase is reset to start the next sector,
   and the disc service is set to allow for the intersector delay.


   Implementation notes:

    1. The 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.
*/

static void end_read (CVPTR cvptr, UNIT *uptr)
{
uint32 limit;

if (cvptr->eod == SET)                                  /* is the end of data indicated? */
    dl_end_command (cvptr, normal_completion);          /* complete the command */

else {                                                  /* reading continues */
    uptr->PHASE = start_phase;                          /* reset to the start phase */
    uptr->wait = cvptr->sector_time;                    /* delay for the intersector time */

    if (cvptr->eoc == SET && cvptr->type == ICD) {      /* seek will be required and controller is ICD? */
        if (!(cvptr->file_mask & DL_FAUTSK))            /* if auto-seek is disabled */
            limit = cvptr->cylinder;                    /*   then the limit is the current cylinder */
        else if (cvptr->file_mask & DL_FDECR)           /* else if enabled and decremental seek */
            limit = 0;                                  /*   then the limit is cylinder 0 */
        else                                            /* else the enabled limit is the last cylinder */
            limit = drive_props [GET_MODEL (uptr->flags)].cylinders;

        if (cvptr->cylinder == limit)                   /* is positioner at the limit? */
            uptr->wait = cvptr->eot_time;               /* seek will fail; delay to allow CPU to untalk */
        }
    }

return;
}


/* Start a write operation on the current sector.

   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.

   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 operation phase unchanged 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 cylinder, the operation phase is set
   for the data transfer, and the index of the first word to transfer is set.


   Implementation notes:

    1. Entry while executing a Write Full Sector or Initialize command inhibits
       address verification.  In addition, the drive's FORMAT switch must be set
       to the enabled position for these commands to succeed.
*/

static void start_write (CVPTR cvptr, UNIT *uptr)
{
const t_bool verify = (CNTLR_OPCODE) uptr->OP == Write; /* only Write verifies the sector address */

if ((uptr->flags & UNIT_WPROT)                          /* is the unit write protected, */
  || !verify && !(uptr->flags & UNIT_FMT))              /*   or is formatting required but not enabled? */
    dl_end_command (cvptr, status_2_error);             /* terminate the write with an error */

else if (position_sector (cvptr, uptr, verify)) {       /* writing is permitted; position the sector */
    uptr->PHASE = data_phase;                           /* positioning succeeded; set up data transfer phase */
    cvptr->index = 0;                                   /* reset the data index */
    }

return;
}


/* Finish a write operation on the current sector.

   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 header words at the start of the buffer that are not
   written to the disc image).

   If the image write failed with a file system error, SCPE_IOERR is returned
   from the service routine to cause a simulation stop; resumption is handled as
   an Uncorrectable Data Error.  If the image was written correctly, the next
   sector address is updated.  If the end-of-data flag is set, the current write
   is completed.  Otherwise, the command phase is reset to start the next
   sector, and the disc service is scheduled to allow for the intersector delay.


   Implementation notes:

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

static t_stat end_write (CVPTR cvptr, UNIT *uptr)
{
uint32 count;
uint16 pad;
const CNTLR_OPCODE opcode = (CNTLR_OPCODE) uptr->OP;
const uint32 offset = (opcode == Write_Full_Sector ? 3 : 0);

if (uptr->flags & UNIT_UNLOAD) {                        /* if the drive is not ready, */
    dl_end_command (cvptr, access_not_ready);           /*   terminate the command with an error */
    return SCPE_OK;
    }

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

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

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

if (ferror (uptr->fileref))                                     /* did a host file system error occur? */
    return io_error (cvptr, uptr, uncorrectable_data_error);    /* set up the data error status and stop the simulation */

next_sector (cvptr, uptr);                              /* address the next sector */

if (cvptr->eod == SET)                                  /* is the end of data indicated? */
    dl_end_command (cvptr, normal_completion);          /* complete the command */

else {                                                  /* writing continues */
    uptr->PHASE = start_phase;                          /* reset to the start phase */
    uptr->wait = cvptr->sector_time;                    /* delay for the intersector time */
    }

return SCPE_OK;
}


/* 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, 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 current 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.
   If the seek is legal, the routine returns with the disc service scheduled for
   seek completion and the command state unchanged.  When the service is
   reentered, 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 disc service scheduled for
   seek completion as above.

   If the drive and controller positions agree or 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 disc service is scheduled 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, t_bool verify)
{
uint32 block;
uint32 model = GET_MODEL (uptr->flags);

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

        start_seek (cvptr, uptr,                                /* start the auto-seek */
                   (CNTLR_OPCODE) uptr->OP,                     /*   with the current operation */
                   (CNTLR_PHASE) uptr->PHASE);                  /*     and phase unchanged */

        if (uptr->STAT & DL_S2SC)                               /* did a seek check occur? */
            if (cvptr->type == ICD)                             /* is this ICD controller? */
                dl_end_command (cvptr, end_of_cylinder);        /* report it as an End of Cylinder error */
            else                                                /* it is a MAC controller */
                dl_end_command (cvptr, status_2_error);         /* report it as a Status-2 error */
        }

    else                                                        /* the file mask does not permit an auto-seek */
        dl_end_command (cvptr, end_of_cylinder);                /*   so terminate with an EOC error */

else if (verify && (uint32) uptr->CYL != cvptr->cylinder) {     /* is the positioner on the wrong cylinder? */
    start_seek (cvptr, uptr,                                    /* start a seek to the correct cylinder */
               (CNTLR_OPCODE) uptr->OP,                         /*   with the current operation */
               (CNTLR_PHASE) uptr->PHASE);                      /*     and phase unchanged */

    if (uptr->STAT & DL_S2SC)                                   /* did a seek check occur? */
        dl_end_command (cvptr, status_2_error);                 /* report a Status-2 error */
    }

else if (((uint32) uptr->CYL >= drive_props [model].cylinders)  /* is the cylinder out of bounds? */
  || (cvptr->head >= drive_props [model].heads)                 /*   or the head? */
  || (cvptr->sector >= drive_props [model].sectors)) {          /*   or the sector? */
    uptr->STAT = uptr->STAT | DL_S2SC;                          /* set Seek Check status */
    dl_end_command (cvptr, status_2_error);                     /*   and terminate with an error */
    }

else if (uptr->flags & UNIT_UNLOAD)                     /* is the drive ready for positioning? */
    dl_end_command (cvptr, access_not_ready);           /* terminate the command with an access error */

else {                                                  /* we are ready to position the image file */
    block = TO_BLOCK (uptr->CYL, cvptr->head,           /* calculate the new block position */
                      cvptr->sector, model);            /*   (for inspection only) */
    uptr->pos = TO_OFFSET (block);                      /*     and then convert to a byte offset */

    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->STAT |= DL_S2FAULT;                           /*   and set Fault status */
        }

    else {                                              /* otherwise the seek succeeded */
        uptr->wait = cvptr->data_time;                  /*   so delay for the data access time */

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

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


/* Address the next sector.

   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.  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 uint32 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)        /* are we at the end of the track? */
    return;                                             /* no, so the next sector value is OK */

cvptr->sector = 0;                                      /* wrap the sector number */

if (cvptr->file_mask & DL_FCYLM) {                      /* are we in cylinder mode? */
    cvptr->head = cvptr->head + 1;                      /* yes, so increment the head */

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

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

cvptr->eoc = SET;                                       /* set the end-of-cylinder flag to */
return;                                                 /*   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.  If the current operation is a recalibrate, a seek is initiated
   to cylinder 0 instead of the cylinder value stored in the controller.  The
   routine returns TRUE if the drive was ready for the seek and FALSE if it was
   not.

   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, and the
   new file offset is calculated.

   A seek check 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 drive operation and phase are set to the supplied values before
   returning.


   Implementation notes:

    1. EOC is not reset for recalibrate so that a reseek will return to the same
       location as was current when the recalibrate was done.

    2. Calculation of the file offset is performed here simply to keep the unit
       position register available for inspection.  The actual file positioning
       is done in position_sector.

    3. 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.
*/

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

if (uptr->flags & UNIT_UNLOAD) {                        /* are the heads unloaded? */
    dl_end_command (cvptr, status_2_error);             /* the seek ends with Status-2 error */
    return FALSE;                                       /*   as the drive was not ready */
    }

if ((CNTLR_OPCODE) uptr->OP == Recalibrate)             /* is the unit recalibrating? */
    target_cylinder = 0;                                /* seek to cylinder 0 and don't reset the EOC flag */

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

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

else {                                                  /* 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->STAT = uptr->STAT | DL_S2SC;                  /*     set Seek Check status */

    else {                                              /* the head and sector are OK */
        uptr->STAT = uptr->STAT & ~DL_S2SC;             /* clear Seek Check status */

        block = TO_BLOCK (uptr->CYL, cvptr->head,       /* set up the new block position */
                          cvptr->sector, model);        /*   (for inspection only) */
        uptr->pos = TO_OFFSET (block);                  /*     and then convert to a byte offset */
        }
    }

if ((uptr->STAT & DL_S2SC) && cvptr->type == ICD)       /* did a Seek Check occur for an ICD controller? */
    dl_end_command (cvptr, status_2_error);             /* the command ends with a Status-2 error */

else {                                                  /* the seek was OK or this is a MAC controller */
    if (delta == 0)                                     /* if the seek is to the same cylinder, */
        delta = 1;                                      /*   then schedule as a one-cylinder seek */

    uptr->wait = cvptr->seek_time * delta;              /* the seek delay is based on the relative movement */
    }

uptr->OP    = next_opcode;                              /* set the next operation */
uptr->PHASE = next_phase;                               /*   and command phase */
return TRUE;                                            /*     and report that the drive was ready */
}


/* Report a stream I/O error.

   Errors indicated by the host file system are reported to the console, and
   simulation is stopped with an "I/O error" message.  If the simulation is
   continued, the CPU will receive the supplied status indication from the
   controller.
*/

static t_stat io_error (CVPTR cvptr, UNIT *uptr, CNTLR_STATUS status)
{
dl_end_command (cvptr, status);                         /* terminate the command with an error */

cprintf ("%s simulator disc library I/O error: %s\n",   /* report the error to the console */
         sim_name, strerror (errno));

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

return SCPE_IOERR;                                      /* return an I/O error to stop the simulator */
}



/* 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] = (uint16) cvptr->cylinder            /* update the cylinder if EOC is set */
                          + (cvptr->eoc == SET ? 1 : 0);

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

return;
}


/* Start or stop the command wait timer.

   A MAC controller uses a 1.8 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 auxiliary timer unit that is activated when the command
   wait timer is started and cancelled when the timer is stopped.

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


   Implementation notes:

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

static void set_timer (CVPTR cvptr, FLIP_FLOP action)
{
if (cvptr->type == MAC)                                 /* is this a MAC controller? */
    if (action == SET)                                  /* should we start the timer? */
        sim_activate_abs (cvptr->aux + timer,           /* activate the auxiliary unit */
                          cvptr->wait_time);
    else                                                /* we stop the timer */
        sim_cancel (cvptr->aux + timer);                /*   by canceling the unit */
return;
}


/* Return the drive status (status word 2).

   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, Drive Fault, First Status, and Seek Check bits are stored
       in the unit status word.  The other status bits are determined
       dynamically.

    2. The Drive Busy bit is set if the unit service is scheduled.  In hardware,
       this bit indicates that the heads are not positioned over a track, i.e.,
       that a seek is in progress.  In simulation, the only time a Request
       Status command is allowed is either when the controller is waiting for
       seek completion or for a new command.  In the latter case, unit service
       will not be scheduled, so activation can only be for seek completion.
*/

static uint16 drive_status (UNIT *uptr)
{
uint16 status;
uint32 model;

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

model = GET_MODEL (uptr->flags);                            /* get the drive model */
status = (uint16) (drive_props [model].type | uptr->STAT);  /* start with the drive type and unit status */

if (uptr->flags & UNIT_WPROT)                           /* is the write protect switch set? */
    status |= DL_S2RO;                                  /* set the Protected status bit */

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

if (uptr->flags & UNIT_DIS)                             /* is the unit non-existent? */
    status |= DL_S2NR;                                  /* set the Not Ready bit */

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

if (sim_is_active (uptr))                               /* is the drive positioner moving? */
    status |= DL_S2BUSY;                                /* set the Drive Busy bit */

if (status & DL_S2ERRORS)                               /* are there any Status-2 errors? */
    status |= DL_S2ERR;                                 /* set the Error bit */

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