/* pdp11_rr.c: RP11/-C/-E/RP02/RPR02/RP03 disk pack device

   Copyright (c) 2022 Tony Lawrence

   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
   ROBERT M SUPNIK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
   IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

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

   Inspired by PDP-11's RK/RL implementations by Robert Supnik.
*/

#if defined(VM_PDP11)  &&  !defined(UC15)

#include <assert.h>

#include "pdp11_defs.h"
#include "sim_disk.h"

/* const_assert() */
#ifndef NDEBUG
#  define _Cx(a, b)        a ## b
#  define _Ax(x)           _Cx(_assert, x)
#  define _Sx(n)           (((n) << 1) - 1)
#  define const_assert(c)  do { static const char _Ax(__LINE__)[-_Sx(!(c))] \
                                = {!(c)};  (void) _Ax(__LINE__); } while (0)
#else
#  define const_assert(c)  (void) 0
#endif

/* Constants */

#define RPCONTR         uint16
#define RPWRDSZ         16
#define MAP_RDW(a,c,b)  (Map_ReadW ((a), (c) << 1, (b)) >> 1)
#define MAP_WRW(a,c,b)  (Map_WriteW((a), (c) << 1, (b)) >> 1)

/* RP02/RPR02 parameters; RP03 doubles # of cylinders (both total and spare) */
#define RP_NUMWD        256                             /* words/sector */
#define RP_NUMCY        203                             /* cylinders/drive */
#define RP_SPARE        3                               /* of those, spare */
#define RP_NUMSF        20                              /* surfaces/cylinder */
#define RP_NUMSC        10                              /* sectors/track */
#define RP_NUMTR        (RP_NUMCY * RP_NUMSF)           /* tracks/drive */
#define RP_NUMBL        (RP_NUMTR * RP_NUMSC)           /* blocks/drive */
#define RP_NUMDR        8                               /* drives/controller */
#define RP_MAXFR        (1 << 16)                       /* max transfer */
#define RP_SIZE(n)      (RP_NUMWD * (n))                /* words in n blocks */
#define RP_SIZE_RP02    RP_SIZE(RP_NUMBL)               /* RP02 capacity, words */
#define RP_SIZE_RP03    RP_SIZE(RP_NUMBL*2)             /* RP03 capacity, words */
#define RP_ROT_12       125                             /* Half rotation, 0.1ms */

#define GET_DA(c,h,s)   (((c) * RP_NUMSF + (h)) * RP_NUMSC + (s))  /* disk addr */

/* Flags in the unit flags word */
#define UNIT_NOAUTO     DKUF_NOAUTOSIZE                 /* autosize disabled */
#define UNIT_V_RP03     (DKUF_V_UF + 0)
#define UNIT_RP03       (1 << UNIT_V_RP03)              /* RP03 vs RP02 (0) */
#define GET_DTYPE(x)    (((x) & UNIT_RP03) >> UNIT_V_RP03)

/* Flags in the device flags word */
#define DEV_V_RP11CE    (DEV_V_FFUF + 0)
#define DEV_RP11CE      (1 << DEV_V_RP11CE)             /* RP11-C/-E vs RP11(0) */

/* Controller / drive types */
#define RP_RP11         "RP11"
#define RP_RP11CE       "RP11-C/-E"
#define RP_RP02         "RP02"
#define RP_RP03         "RP03"

/* Parameters in the unit descriptor */
#define CYL             u3                              /* current cylinder */
#define HEAD            u4                              /* current track */
#define FUNC            us9                             /* current func */
#define STATUS          u5                              /* RPDS bits for the unit */
#define SEEKING         u6                              /* seek cmd underway */

/* 4 dummy UNIBUS registers followed by... */
#define RP_IOFF         4
/* 12 real UNIBUS registers, so 16 registers total: 32 addresses */
#define RP_IOLN         040

/* RP(R)02/RP03(RP02P) particulars (all disks rotated at 2400rpm) */
static struct drv_typ {
    const char*         name;                           /* device type name */
    int32               cyl;                            /* cylinders */
    int32               size;                           /* #blocks */
    int32               spare;                          /* spare (out of cyl) */
    int32               seek_1;                         /* one track move, 0.1ms */
    int32               seek_ave;                       /* average seek, 0.1ms */
    int32               seek_max;                       /* maximal seek, 0.1ms */
} drv_tab[] = {
    { RP_RP02, RP_NUMCY,   RP_NUMBL,   RP_SPARE,  200, 500, 800 },
    { RP_RP03, RP_NUMCY*2, RP_NUMBL*2, RP_SPARE*2, 75, 290, 550 }
};

/* RPDS 776710, selected drive status, read-only except for the attention bits */
static BITFIELD rp_ds_bits[] = {
#define RPDS_ATTN       0000377                         /* attention (read/clear) */
    BITF(ATTN,8),
#define RPDS_WLK        0000400                         /* write locked */
    BIT(WLK),
#define RPDS_UNSAFE     0001000                         /* unsafe */
    BIT(UNSAFE),
#define RPDS_SEEK       0002000                         /* seek underway */
    BIT(SEEK),
#define RPDS_INC        0004000                         /* seek incomplete */
    BIT(INC),
#define RPDS_HNF        0010000                         /* header not found */
    BIT(HNF),
#define RPDS_RP03       0020000                         /* drive is RP03 */
    BIT(RP03=),
#define RPDS_ONLN       0040000                         /* unit online */
    BIT(ONLN),
#define RPDS_RDY        0100000                         /* unit ready */
    BIT(RDY),
    ENDBITS
};
#define RPDS_REAL       0017000                         /* bits stored */
#define RPDS_DKER       (RPDS_HNF | RPDS_INC)           /* drive error */
#define RPER_DKER(x)    ((x) & RPDS_DKER ? RPER_DRE : 0)/* DRE for RPER */

/* RPER 776712, error register, read-only */
static BITFIELD rp_er_bits[] = {
#define RPER_DRE        0000001                         /* drive error (HNF|INC) */
    BIT(DRE),
#define RPER_EOP        0000002                         /* end of pack (overrun) */
    BIT(EOP),
#define RPER_NXM        0000004                         /* nx memory */
    BIT(NXM),
#define RPER_WCE        0000010                         /* write check error */
    BIT(WCE),
#define RPER_TE         0000020                         /* timing error */
    BIT(TE),
#define RPER_CSE        0000040                         /* serial checksum error */
    BIT(CSE),
#define RPER_WPE        0000100                         /* word parity error */
    BIT(WPE),
#define RPER_LPE        0000200                         /* longitudinal parity error */
    BIT(LPE),
#define RPER_MODE       0000400                         /* mode error */
    BIT(MODE),
#define RPER_FMTE       0001000                         /* format error */
    BIT(FMTE),
#define RPER_PGE        0002000                         /* programming error */
    BIT(PGE), 
#define RPER_NXS        0004000                         /* nx sector */
    BIT(NXS),
#define RPER_NXT        0010000                         /* nx track */
    BIT(NXT),
#define RPER_NXC        0020000                         /* nx cylinder */
    BIT(NXC),
#define RPER_FUV        0040000                         /* unsafe violation */
    BIT(FUV),
#define RPER_WPV        0100000                         /* write lock violation */
    BIT(WPV),
    ENDBITS
};
#define RPER_REAL       0177776
/* hard errors: drawing 19 */
#define RPER_HARDERR    (RPER_WPV | RPER_FUV | RPER_NXC | RPER_NXT | \
                         RPER_NXS | RPER_PGE | RPER_NXM | RPER_DRE | RPER_MODE)
/* soft errors: drawing 19 */
#define RPER_SOFTERR    (RPER_LPE | RPER_WPE | RPER_CSE | RPER_WCE | \
                         RPER_EOP | RPER_TE  | RPER_FMTE)
#define RPER_HARD(x)    ((x) & RPER_HARDERR ? (RPCS_ERR | RPCS_HERR) : 0)
#define RPER_SOFT(x)    ((x) & RPER_SOFTERR ?  RPCS_ERR              : 0)

/* RPCS 776714, command/status register */
static const char* rp_funcs[] = {
    "RESET", "WRITE", "READ", "WCHK", "SEEK", "WRNOSEEK", "HOME", "RDNOSEEK"
};
static BITFIELD rp_cs_bits[] = {
/* CSR_GO */                                            /* the GO! bit */
    BIT(GO),         /* 0000001 */
#define RPCS_V_FUNC     1
#define RPCS_M_FUNC     7
#define RPCS_FUNC       (RPCS_M_FUNC << RPCS_V_FUNC)    /* function: */
#define  RPCS_RESET     0                               /*  Idle (Reset), i */
#define  RPCS_WRITE     1                               /*  Write, x */
#define  RPCS_READ      2                               /*  Read, x */
#define  RPCS_WCHK      3                               /*  Write Check, x */
#define  RPCS_SEEK      4                               /*  Seek, i */
#define  RPCS_WR_NOSEEK 5                               /*  Write (No Seek), x */
#define  RPCS_HOME      6                               /*  Home Seek, i */
#define  RPCS_RD_NOSEEK 7                               /*  Read (No Seek), x */
    BITFNAM(FUNC,3,rp_funcs),                           /*  i: Initiate; x: Execute */
                     /* 0000016 */
#define RPCS_V_MEX      4
#define RPCS_M_MEX      3
#define RPCS_MEX        (RPCS_M_MEX << RPCS_V_MEX)      /* memory extension */
    BITF(MEX,2),     /* 0000060 */
/* CSR_IE */                                            /* interrupt enable */
    BIT(IE),         /* 0000100 */
/* CSR_DONE */                                          /* controller ready */
    BIT(DONE),       /* 0000200 */
#define RPCS_V_DRV      8
#define RPCS_M_DRV      7
#define RPCS_DRV        (RPCS_M_DRV << RPCS_V_DRV)      /* drive id */
    BITFFMT(DRV,3,%u),/*0003400 */
#define RPCS_HDR        0004000                         /* header operation */
    BIT(HDR),
#define RPCS_MODE       0010000                         /* 0=PDP-11; 1=PDP-10/-15 or format */
    BIT(MODE),
#define RPCS_AIE        0020000                         /* attention interrupt enable */
    BIT(AIE),
#define RPCS_HERR       0040000                         /* hard error */
    BIT(HERR),
#define RPCS_ERR        CSR_ERR                         /* error (hard or soft) */
    BIT(ERR),        /* 0100000 */
#define RPCS_REAL       0037776                         /* bits kept here */
#define RPCS_RW         0037576                         /* read/write */
#define GET_FUNC(x)     (((x) & RPCS_FUNC) >> RPCS_V_FUNC)
#define GET_DRIVE(x)    (((x) & RPCS_DRV) >> RPCS_V_DRV)
    ENDBITS
};

/* RPWC 776716, two's complement word count */
/* For PDP-11 must be even for data, and in multiples of 3 for format */
static BITFIELD rp_wc_bits[] = {
    BITFFMT(WC,16,%u),
#define RPWC_IMP        0177777                         /* implemented */
    ENDBITS
};

/* RPBA 776720, bus address */
static BITFIELD rp_ba_bits[] = {
    BITF(BA,16),
#define RPBA_IMP        0177776                         /* implemented */
    ENDBITS
};

/* RPCA 776722, cylinder address */
static BITFIELD rp_ca_bits[] = {
    BITFFMT(CYL,9,%u),
#define RPCA_RW         0000777                         /* RP11: 0377 */
    ENDBITS
};
static BITFIELD rp_ca11_bits[] = {                      /* RP11 version */
#define RPCA_M_CYL      0000377
    BITFFMT(CYL,8,%u),
#define RPCA_V_SUCA     8
#define RPCA_SUCA       (RPCA_M_CYL << RPCA_V_SUCA)
    BITFFMT(SUCA,8,%u),/*177400*/
#define RPCA_IMP        0177777                         /* implemented */
    ENDBITS
};

/* RPDA 776724, disk address (track/sector) */
static BITFIELD rp_da_bits[] = {
#define RPDA_M_SECT     017
#define RPDA_SECT       RPDA_M_SECT                     /* sector */
    BITFFMT(SECT,4,%u),/*000017*/
#define RPDA_V_SOT      4
#define RPDA_SOT        (RPDA_M_SECT << RPDA_V_SOT)     /* current sect on track */
    BITFFMT(SOT,4,%u), /*000360*/
#define RPDA_V_TRACK    8
#define RPDA_M_TRACK    037
#define RPDA_TRACK      (RPDA_M_TRACK << RPDA_V_TRACK)  /* track */
    BITFFMT(SURF,5,%u),/*017400*/
#define RPDA_RW         0017417                         /* read/write */
#define RPDA_IMP        0017777                         /* implemented */
#define GET_SECT(x)     ((x) & RPDA_SECT)
#define GET_TRACK(x)    (((x) & RPDA_TRACK) >> RPDA_V_TRACK)
    ENDBITS
};

/* RPM1 776726 maintenance 1, read-only, not implemented */

/* RPM2 776730 maintenance 2, read-only, not implemented */

/* RPM1 776732 maintenance 3, write-only, not implemented */

/* SUCA 776734 selected unit cylinder address, read-only */
static BITFIELD rp_suca_bits[] = {
    BITFFMT(CYL,9,%u),                                  /* RP11: RPCA<15:08> */
#define SUCA_IMP        0000777                         /* RP11: 0377 */
    ENDBITS
};

/* SILO 776736 silo memory, not implemented */

/* Maintenance Write Lockout Address (LOA) (the switches on the maint. panel) */
static const char* offon[] = { "OFF", "ON" };
static BITFIELD rp_wloa_bits[] = {
#define RPWLOA_CYL      0000377                         /* cyls locked */
    BITFFMT(CYL,8,%u),
#define RPWLOA_V_DRV    8
#define RPWLOA_M_DRV    7
#define RPWLOA_DRV      (RPWLOA_M_DRV << RPWLOA_V_DRV)  /* drives locked */
    BITFFMT(DRV,3,%u),/*0003400*/
#define RPWLOA_IMP      0003777                         /* implemented */
#define GET_WLOA(n, c)  (((n) << RPWLOA_V_DRV) |                            \
                         (rr_dev.flags & DEV_RP11CE ? (c) >> 1 : (c)))
    BITNCF(4),
#define RPWLOA_ON       0100000                         /* sw on/off switch */
    BITFNAM(PROTECT,1,offon),
    ENDBITS
};

/* Data buffer and device registers */

static RPCONTR* rpxb = NULL;                            /* xfer buffer */
static int32 rpds = 0;                                  /* drive status */
static int32 rper = 0;                                  /* error status */
static int32 rpcs = 0;                                  /* control/status */
static int32 rpwc = 0;                                  /* word count */
static int32 rpba = 0;                                  /* memory address */
static int32 rpca = 0;                                  /* cylinder address */
static int32 rpda = 0;                                  /* disk address */
static int32 suca = 0;                                  /* current cylinder address */
static int32 wloa = 0;                                  /* write lockout address */
static int32 not_impl = 0;                              /* dummy register value */

/* Debug detail levels */

#define RRDEB_OPS       001                             /* transactions */
#define RRDEB_RRD       002                             /* reg reads */
#define RRDEB_RWR       004                             /* reg writes */
#define RRDEB_TRC       010                             /* trace */
#define RRDEB_INT       020                             /* interrupts */
#define RRDEB_DAT      0100                             /* transfer data */

static DEBTAB rr_deb[] = {
    { "OPS",       RRDEB_OPS, "transactions" },
    { "RRD",       RRDEB_RRD, "register reads" },
    { "RWR",       RRDEB_RWR, "register writes" },
    { "INTERRUPT", RRDEB_INT, "interrupts" },
    { "TRACE",     RRDEB_TRC, "trace" },
    { "DATA",      RRDEB_DAT, "transfer data" },
    { NULL, 0 }
};

static struct {
    const char* name;
    int32*      valp;
    BITFIELD*   bits;
} rr_regs[] = {
    { "RPDS", &rpds,     rp_ds_bits   },
    { "RPER", &rper,     rp_er_bits   },
    { "RPCS", &rpcs,     rp_cs_bits   },
    { "RPWC", &rpwc,     rp_wc_bits   },
    { "RPBA", &rpba,     rp_ba_bits   },
    { "RPCA", &rpca,     rp_ca_bits   },
    { "RPDA", &rpda,     rp_da_bits   },
    { "RPM1", &not_impl, NULL         },
    { "RPM2", &not_impl, NULL         },
    { "RPM3", &not_impl, NULL         },
    { "SUCA", &suca,     rp_suca_bits },
    { "SILO", &not_impl, NULL         }
};

/* Forward decls */

static t_stat rr_rd (int32 *data, int32 PA, int32 access);
static t_stat rr_wr (int32 data, int32 PA, int32 access);
static int32  rr_inta (void);
static t_stat rr_svc (UNIT *uptr);
static t_stat rr_reset (DEVICE *dptr);
static void   rr_go (int16 func);
static void   rr_set_done (int32 error);
static void   rr_clr_done (void);
static t_stat rr_boot (int32 unitno, DEVICE *dptr);
static t_stat rr_help (FILE *st, DEVICE *dptr, UNIT *uptr, int32 flag, const char *cptr);
static t_stat rr_attach (UNIT *uptr, CONST char *cptr);
static t_stat rr_detach (UNIT *uptr);
static t_stat rr_set_type (UNIT *uptr, int32 val, CONST char *cptr, void *desc);
static t_stat rr_show_type (FILE *st, UNIT *uptr, int32 val, CONST void *desc);
static t_stat rr_set_wloa (UNIT *uptr, int32 val, CONST char *cptr, void *desc);
static t_stat rr_set_ctrl (UNIT *uptr, int32 val, CONST char *cptr, void *desc);
static t_stat rr_show_ctrl (FILE *st, UNIT *uptr, int32 val, CONST void *desc);
static const char *rr_description (DEVICE *dptr);

/* RP11 data structures

   rr_dib       RR device context for PDP-11
   rr_reg       RR register list
   rr_unit      RR unit list
   rr_mod       RR modifier list
   rr_dev       RR device descriptor
*/

static DIB rr_dib = {
    IOBA_AUTO/*base address*/, RP_IOLN/*addresses used*/,
    rr_rd, rr_wr,
    1/*# of vectors*/, IVCL(RR)/*locator*/, VEC_AUTO,
    { rr_inta }, RP_IOLN/*addresses per device*/,
};

static REG rr_reg[] = {
    /* registers: CSR first, then in the bus address order */
    { ORDATADF(RPCS, rpcs, 16, "control/status",        rp_cs_bits) },
    { ORDATADF(RPDS, rpds, 16, "drive status",          rp_ds_bits) },
    { ORDATADF(RPER, rper, 16, "error status",          rp_er_bits) },
    { ORDATADF(RPWC, rpwc, 16, "word count",            rp_wc_bits) },
    { ORDATADF(RPBA, rpba, 16, "memory address",        rp_ba_bits) },
    { ORDATADF(RPCA, rpca, 16, "cylinder address",      rp_ca_bits) },
    { ORDATADF(RPDA, rpda, 16, "disk address",          rp_da_bits) },
    { ORDATADF(SUCA, suca, 16, "current cylinder",      rp_suca_bits), REG_RO },
    { ORDATADF(WLOA, wloa, 16, "write lockout address", rp_wloa_bits) },

    /* standard stuff */
    { FLDATAD (INT,  IREQ(RR), INT_V_RR,   "interrupt pending flag") },
    { FLDATAD (ERR,  rpcs,     CSR_V_ERR,  "error flag (CSR<15>)") },
    { FLDATAD (DONE, rpcs,     CSR_V_DONE, "device done flag (CSR<7>)") },
    { FLDATAD (IE,   rpcs,     CSR_V_IE,   "interrupt enable flag (CSR<6>)") },
    { ORDATA  (DEVADDR, rr_dib.ba,  32), REG_HRO },
    { ORDATA  (DEVVEC,  rr_dib.vec, 16), REG_HRO },
    { NULL }
};

static UNIT rr_unit[RP_NUMDR] = {
    { UDATA(rr_svc,
            UNIT_FIX | UNIT_ATTABLE | UNIT_DISABLE | UNIT_ROABLE | UNIT_RP03,
            RP_SIZE_RP03) },
    { UDATA(rr_svc,
            UNIT_FIX | UNIT_ATTABLE | UNIT_DISABLE | UNIT_ROABLE | UNIT_RP03,
            RP_SIZE_RP03) },
    { UDATA(rr_svc,
            UNIT_FIX | UNIT_ATTABLE | UNIT_DISABLE | UNIT_ROABLE | UNIT_RP03,
            RP_SIZE_RP03) },
    { UDATA(rr_svc,
            UNIT_FIX | UNIT_ATTABLE | UNIT_DISABLE | UNIT_ROABLE | UNIT_RP03,
            RP_SIZE_RP03) },
    { UDATA(rr_svc,
            UNIT_FIX | UNIT_ATTABLE | UNIT_DISABLE | UNIT_ROABLE | UNIT_RP03,
            RP_SIZE_RP03) },
    { UDATA(rr_svc,
            UNIT_FIX | UNIT_ATTABLE | UNIT_DISABLE | UNIT_ROABLE | UNIT_RP03,
            RP_SIZE_RP03) },
    { UDATA(rr_svc,
            UNIT_FIX | UNIT_ATTABLE | UNIT_DISABLE | UNIT_ROABLE | UNIT_RP03,
            RP_SIZE_RP03) },
    { UDATA(rr_svc,
            UNIT_FIX | UNIT_ATTABLE | UNIT_DISABLE | UNIT_ROABLE | UNIT_RP03,
            RP_SIZE_RP03) }
};

static MTAB rr_mod[] = {
    { MTAB_VDV, 0,
        "TYPE",         NULL, 
        NULL,           rr_show_ctrl,   NULL,
        "Display controller type" },
    { MTAB_VDV, 0/*RP11*/,
        NULL,           RP_RP11, 
        rr_set_ctrl,    NULL,           NULL,
        "Set " RP_RP11 " controller type" },
    { MTAB_VDV, DEV_RP11CE,
        NULL,           "RP11CE", 
        rr_set_ctrl,    NULL,           NULL,
        "Set " RP_RP11CE " controller type" },
    { MTAB_VDV | MTAB_VALR, 0,
        NULL,           "PROTECT",
        rr_set_wloa,    NULL,           NULL,
        "Set write lockout mode/address" },
    { MTAB_VUN, 0,
        "WRITEENABLED", "WRITEENABLED", 
        set_writelock,  show_writelock, NULL,
        "Write enable disk drive" },
    { MTAB_VUN, 1,
        NULL,           "LOCKED", 
        set_writelock,  NULL,           NULL,
        "Write lock disk drive" },
    { MTAB_VUN, 0,
        "TYPE",         NULL,
        NULL,           rr_show_type,   NULL,
        "Display device type" },
    { MTAB_VUN, 0/*RP02*/,
        NULL,           RP_RP02,
        rr_set_type,    NULL,           NULL,
        "Set " RP_RP02 " disk type" },
    { MTAB_VUN, UNIT_RP03,
        NULL,           RP_RP03,
        rr_set_type,    NULL,   NULL,
        "Set " RP_RP03 " disk type (only " RP_RP11CE ")"},
    { UNIT_NOAUTO, 0,
        "autosize",     "AUTOSIZE", 
        NULL,           NULL,           NULL,
        "Set type based on file size at attach" },
    { UNIT_NOAUTO, UNIT_NOAUTO,
        "noautosize",   "NOAUTOSIZE",   NULL,
        NULL,           NULL,
        "Disable disk autosize on attach" },
    { MTAB_VUN | MTAB_VALR, 0,
        "FORMAT",       "FORMAT={AUTO|SIMH|VHD|RAW}",
        sim_disk_set_fmt, sim_disk_show_fmt, NULL,
        "Set/Display disk format" },
    { MTAB_VDV | MTAB_VALR, RP_IOLN/*modulus*/,
        "ADDRESS",      "ADDRESS",
        set_addr,       show_addr,      NULL,
        "Bus address" },
    { MTAB_VDV | MTAB_VALR, 0,
        "VECTOR",       "VECTOR",
        set_vec,        show_vec,       NULL,
        "Interrupt vector" },
    { 0 }
};

DEVICE rr_dev = {
    "RR", rr_unit, rr_reg, rr_mod, RP_NUMDR,
    DEV_RDX/*address radix*/, 26/*address width*/, 1/*address increment*/,
    DEV_RDX/*data radix*/, RPWRDSZ/*data width*/,
    NULL/*examine()*/, NULL/*deposit()*/,
    rr_reset, rr_boot, rr_attach, rr_detach,
    &rr_dib,
    DEV_DIS | DEV_DISABLE | DEV_UBUS | DEV_Q18 | DEV_DEBUG | DEV_DISK | DEV_RP11CE,
    0/*debug control*/, rr_deb,
    NULL/*msize()*/, NULL/*logical name*/,
    rr_help, NULL/*attach_help()*/, NULL/*help_ctx*/,
    rr_description
};

/* I/O dispatch routine, I/O addresses 17776710 - 17776736

   17776710     RPDS    read-only except for attention bits
   17776712     RPER    read-only
   17776714     RPCS    read/write
   17776716     RPWC    read/write
   17776720     RPBA    read/write
   17776722     RPCA    read/write
   17776724     RPDA    read/write
   17776726     RPM1    read-only,  unimplemented
   17776730     RPM2    read-only,  unimplemented
   17776732     RPM3    write-only, unimplemented
   17776734     SUCA    read-only
   17776736     SILO    read/write, unimplemented

http://www.bitsavers.org/pdf/dec/unibus/RP11-C_schemMay1973.pdf
http://www.bitsavers.org/pdf/dec/unibus/DEC-11-HRPCA-C-D_RP11-C_Maint_Aug74.pdf

RP11-C actually responds to the range 17776700 - 17776736 with the first 4 word
locations unused.

Some operating systems want you to specify the latter range (RSTS/E), but some
just want to know where the CSR is located, so they auto-calculate the range.

The _original_ RP11 had the following differences:  it responded to the address
range 17776710 - 17776746:  RPM3 was followed by 3 buffer registers RPB1 - RPB3,
then 3 locations (42-46) were unused.  RPCA was both the cylinder address in the
lower 8 bits <07:00> (read-write), and the Selected Unit current cylinder address
(a la SUCA in RP11-C) in the higher 8 bits <15:08> (read-only).  Since only the
RP02 disk drives were supported, it only required 8 bits for cylinder addresses.
There were no separate SUCA/SILO registers (the locations were occupied by RPBs).
The RP03 bit in RPDS was always 0.  But programmatically RP11 was mostly compatible
with the -C revision (except for the SU current cylinder address, which was not
widely used by major software, anyways).

http://www.bitsavers.org/pdf/dec/unibus/RP11_schem_Sep74.pdf

RP11-E was just a newer version of RP11-C and supported both RP02(or RPR02) and
RP03 disk drives on the same controller.

*/

static t_stat rr_rd (int32 *data, int32 PA, int32 access)
{
    /* offset by base then decode <4:1> */
    int32 rn = (((PA - rr_dib.ba) & (RP_IOLN - 1)) >> 1) - RP_IOFF;
    BITFIELD* bits = 0;
    UNIT* uptr;

    switch (rn) {
    case 0:                                             /* RPDS */
    case 1:                                             /* RPER */
    case 2:                                             /* RPCS */
        /* RPDS */
        uptr = rr_dev.units + GET_DRIVE(rpcs);          /* selected unit */
        rpds &= RPDS_ATTN;                              /* attention bits */
        if (!(uptr->flags & UNIT_DIS)) {                /* not disabled? */
            rpds |= RPDS_ONLN;
            if (GET_DTYPE(uptr->flags))                 /* RP03? */
                rpds |= RPDS_RP03;
            if (uptr->flags & UNIT_ATT) {               /* attached? */
                rpds |= uptr->STATUS & RPDS_REAL;
                if (uptr->flags & UNIT_WPRT)            /* write locked? */
                    rpds |= RPDS_WLK;
                if (uptr->SEEKING)                      /* still seeking? */
                    rpds |= RPDS_SEEK;
                else if (!uptr->FUNC  &&  !(rpds & (RPDS_INC | RPDS_UNSAFE)))
                    rpds |= RPDS_RDY;                   /* ready! */
            } else
                rpds |= uptr->STATUS & (RPDS_DKER | RPDS_UNSAFE);
        }

        /* RPER */
        rper &= RPER_REAL;
        rper |= RPER_DKER(rpds); 

        /* RPCS */
        rpcs &= RPCS_REAL;
        rpcs |= RPER_HARD(rper) | RPER_SOFT(rper);

        *data = *rr_regs[rn].valp;
        break;

    case 3:                                             /* RPWC */
        *data = rpwc & RPWC_IMP;
        break;

    case 4:                                             /* RPBA */
        *data = rpba & RPBA_IMP;
        break;

    case 5:                                             /* RPCA */
        const_assert(RPCA_M_CYL == (RPCA_RW >> 1));
        if (!(rr_dev.flags & DEV_RP11CE)) {             /* RP11? */
            *data = (suca << RPCA_V_SUCA) | (rpca & RPCA_M_CYL);
            bits = rp_ca11_bits;                        /* RP11 */
        } else
            *data = rpca & RPCA_RW;
        break;

    case 6:                                             /* RPDA */
        uptr = rr_dev.units + GET_DRIVE(rpcs);
        if (uptr->flags & UNIT_ATT) {
            rpda &= ~RPDA_SOT;
            rpda |= (rand() % RP_NUMSC) << RPDA_V_SOT;  /* inject a random sect */
        }
        *data = rpda & RPDA_IMP;
        break;

    case 10:                                            /* SUCA */
        if (rr_dev.flags & DEV_RP11CE) {
            *data = suca;
            break;
        }
        /* FALLTHRU */
    default:                                            /* not implemented */
        *data = 0;
        return SCPE_OK;
    }
    sim_debug(RRDEB_RRD, &rr_dev, ">>RR  read: %s=%#o\n", rr_regs[rn].name, *data);
    sim_debug_bits(RRDEB_RRD, &rr_dev, bits ? bits : rr_regs[rn].bits, *data, *data, 1);
    return SCPE_OK;
}

#define RR_DATOB(r, d)  (PA & 1 ? ((d) << 8) | ((r) & 0377) : ((r) & ~0377) | (d))

static t_stat rr_wr (int32 data, int32 PA, int32 access)
{
    /* offset by base then decode <4:1> */
    int32 rn = (((PA - rr_dib.ba) & (RP_IOLN - 1)) >> 1) - RP_IOFF;
    int32 n, oval = rn < 0 ? 0 : *rr_regs[rn].valp;
    int16 func;

    assert(rn < (int32)(sizeof(rr_regs)/sizeof(rr_regs[0])));
    if (access == WRITEB  &&  2 <= rn  &&  rn <= 6)
        data = RR_DATOB(oval, data);
    switch (rn) {
    case 0:                                             /* RPDS */
        if (access != WRITEB  ||  !(PA & 1)) {
            rpds &= ~(data & RPDS_ATTN);                /* clr attention bits */
            if (!(rpds & RPDS_ATTN)  &&  (rpcs & RPCS_AIE)
                &&  (!(rpcs & CSR_IE)  ||  !(rpcs & CSR_DONE))) {
                sim_debug(RRDEB_INT, &rr_dev, "rr_wr(ATTN:CLR_INT)\n");
                CLR_INT(RR);                            /* clr int request */
            }
        }
        break;

    case 1:                                             /* RPER: read-only */
        break;

    case 2:                                             /* RPCS */
        if (((data & CSR_IE)
             &&  (rpcs & (CSR_DONE | CSR_IE)) == CSR_DONE)  ||
            ((data & RPCS_AIE)
             &&  !(rpcs & RPCS_AIE)  &&  (rpds & RPDS_ATTN))) {
            sim_debug(RRDEB_INT, &rr_dev, "rr_wr(CSR:SET_INT)\n");
            SET_INT(RR);                                /* set int request */
        } else if (rpcs & (CSR_IE | RPCS_AIE)) {
            sim_debug(RRDEB_INT, &rr_dev, "rr_wr(CSR:CLR_INT)\n");
            CLR_INT(RR);                                /* clr int request */
        }
        rpcs &= ~(RPCS_RW | CSR_GO);
        rpcs |= data & RPCS_RW;
        n = GET_DRIVE(rpcs);                            /* get drive no */
        if (n != GET_DRIVE(oval)) {
            UNIT* uptr = rr_dev.units + n;              /* new selected unit */
            suca = uptr->CYL;
            n = 1;
        } else
            n = 0;                                      /* same old */
        func = GET_FUNC(rpcs);                          /* get function */
        if (((rpcs & CSR_DONE)  ||  func == RPCS_RESET) /* ready or reset? */
            &&   (data & CSR_GO)) {                     /* ...and GO? */
            rr_go(func);                                /* new function! */
        } else if (!(rpcs & CSR_DONE)                   /* not ready? */
            &&  ((data & CSR_GO) | n)) {                /* ...and: GO or desel? */
            rper |= RPER_PGE;                           /* flag error */
        }
        break;

    case 3:                                             /* RPWC */
        rpwc = data & RPWC_IMP;
        break;

    case 4:                                             /* RPBA */
        rpba = data & RPBA_IMP;
        break;

    case 5:                                             /* RPCA */
        const_assert((RPCA_RW >> 1) == RPCA_M_CYL);
        rpca = data & (rr_dev.flags & DEV_RP11CE ? RPCA_RW : RPCA_M_CYL);
        break;

    case 6:                                             /* RPDA */
        rpda &= RPDA_IMP;
        rpda &= ~RPDA_RW;
        rpda |= data & RPDA_RW;
        break;

    case 10:                                            /* SUCA: read-only */
        if (rr_dev.flags & DEV_RP11CE)
            break;
        /*FALLTHRU*/
    default:
        return SCPE_OK;
    }
    sim_debug(RRDEB_RWR, &rr_dev, ">>RR write: %s=%#o\n", rr_regs[rn].name, data);
    /* note that this is post-op; so e.g. it won't ever show the GO bit as 1 */
    sim_debug_bits(RRDEB_RWR, &rr_dev, rr_regs[rn].bits, oval, *rr_regs[rn].valp, 1);
    return SCPE_OK;
}

/* Complete seek initiation */

static t_stat rr_seek_init (UNIT *uptr)
{
    rr_set_done(0);                                     /* set done */
    assert(uptr->SEEKING);
    uptr->action = rr_svc;
    sim_activate(uptr, uptr->SEEKING);                  /* seek ends then */
    return SCPE_OK;
}

/* Initiate new function */

static void rr_go (int16 func)
{
    int32 i, n, type, cyl, head;
    t_bool rd, wr;
    UNIT* uptr;

    assert(func == GET_FUNC(rpcs));

    if (func == RPCS_RESET) {                           /* control reset? */
        rpds = 0;
        rper = 0;
        rpcs = CSR_DONE | (rpcs & CSR_IE);
        rpwc = 0;
        rpba = 0;
        rpca = 0;
        rpda = 0;
        suca = rr_dev.units[0].CYL;
        for (i = 0;  i < RP_NUMDR;  ++i) {
            uptr = rr_dev.units + i;
            sim_cancel(uptr);
            uptr->action = rr_svc;
            uptr->SEEKING = 0;
            uptr->STATUS = 0;
            uptr->FUNC = 0;
        }
        if (rpcs & CSR_IE) {
            sim_debug(RRDEB_INT, &rr_dev, "rr_go(RESET:SET_INT)\n");
            SET_INT(RR);                                /* set int request */
        } else {
            sim_debug(RRDEB_INT, &rr_dev, "rr_go(RESET:CLR_INT)\n");
            CLR_INT(RR);                                /* clr int request */
        }
        return;
    }

    assert(rpcs & CSR_DONE);

    rr_clr_done();                                      /* clear done */
    rper  = 0;                                          /* clear errors */
    rpcs &= ~(CSR_ERR | RPCS_HERR);                     /* clear summary */
    i = GET_DRIVE(rpcs);                                /* get drive no */
    uptr = rr_dev.units + i;                            /* selected unit */
    assert(uptr->action == rr_svc);
    assert(uptr->SEEKING  ||  !uptr->FUNC);             /* SEEK underway or idle */
    uptr->STATUS &= ~RPDS_DKER;                         /* clear drive errors */

    if (!(uptr->flags & UNIT_ATT)) {                    /* not attached? */
        rr_set_done(RPER_PGE);                          /* unit offline */
        return;
    }
    if (uptr->STATUS & RPDS_UNSAFE) {                   /* file unsafe? */
        rr_set_done(RPER_FUV);                          /* unsafe violation */
        return;
    }

    assert(!uptr->FUNC  ||  uptr->FUNC == RPCS_HOME  ||  uptr->FUNC == RPCS_SEEK);
    if ((uptr->FUNC == RPCS_HOME  &&  func != RPCS_HOME)  ||
        (uptr->FUNC == RPCS_SEEK  &&  func == RPCS_SEEK)) {
        rr_set_done(RPER_PGE);                          /* no can't do */
        return;
    }
    /* RP11 allows to stack up an I/O command on top of an ongoing SEEK.  The
     * I/O gets performed once the SEEK has completed (with updated DA from the
     * I/O command).  XXDP checks that.
     * RPDS_SEEK in the unit's STATUS means that drive is performing a SEEK as
     * an initiation for an I/O (which can be a part of the topped SEEK).  OTOH,
     * SEEKING denotes that either a HOME/SEEK command is in progress (per FUNC)
     * or SEEK was in progress before the stacked I/O command (stored in FUNC). */
    assert(!(uptr->STATUS & RPDS_SEEK));

    rd = func == RPCS_READ   ||  func == RPCS_RD_NOSEEK  ||  func == RPCS_WCHK;
    wr = func == RPCS_WRITE  ||  func == RPCS_WR_NOSEEK;

    if (wr  &&  (uptr->flags & UNIT_WPRT))              /* write and locked? */
        rper |= RPER_WPV;

    type = GET_DTYPE(uptr->flags);                      /* get drive type */

    if (func == RPCS_HOME) {
        cyl  = 0;
        head = 0;
    } else if (func == RPCS_RD_NOSEEK  ||  func == RPCS_WR_NOSEEK) {
        cyl  = uptr->CYL;
        head = uptr->HEAD;
        assert(head < RP_NUMSF  &&  cyl < drv_tab[type].cyl);
    } else {
        cyl  = rpca;
        head = GET_TRACK(rpda);
        if (cyl >= drv_tab[type].cyl)                   /* bad cyl? */
            rper |= RPER_NXC;
        if (head >= RP_NUMSF)                           /* bad head? */
            rper |= RPER_NXT;
    }
    if (rd | wr) {
        int32 sect = GET_SECT(rpda);                    /* get sect */
        if (sect >= RP_NUMSC)                           /* sect out of range? */
            rper |= RPER_NXS;
    }

    if (rper) {                                         /* any errors? */
        rr_set_done(0);                                 /* set done (w/errors) */
        return;
    }

    /* seek time */
    n = cyl - uptr->CYL;                                /* cyl departure */
    if (func == RPCS_HOME)
        i  = drv_tab[type].seek_ave / 2;
    else if (!(i = abs(n)))
        i  = drv_tab[type].seek_1 / 2;
    else if (i <= 2)
        i *= drv_tab[type].seek_1;
    else if (i <= (3 * drv_tab[type].cyl) / 4)
        i  = drv_tab[type].seek_ave;
    else
        i  = drv_tab[type].seek_max;
    if (func == RPCS_HOME  ||  func == RPCS_SEEK) {     /* seek? */
        uptr->action = rr_seek_init;
        uptr->SEEKING = i;                              /* drive is seeking */
        assert(i);
        i = 10;                                         /* enough for 16us */
        /* XXDP ZRPF-B, Test 0 (cylinder positioning) fails because, even though
         * it initializes INTFLG late (_after_ initiating the SEEK), the delayed
         * CS_DONE here helps avoid the race (and an error) but only for the
         * first iteration (cylinder 0).  The next and all subsequent iterations
         * begin with clearing attention bits in RDPS with a BIC instruction,
         * like so: "BIC @#RPDS, @#RPDS", resulting in bare 0 actually sent by
         * ALU to the UNIBUS in DATO cycle to RPDS, and causing no bits reset.
         * That, in turn, causes the interrupt to occur immediately when RPCS is
         * written with the new SEEK/AIE function (the attention bit still set)
         * for the next cylinder, and that causes INTFLG to get set by the ISR
         * just prior to when the test clears it.  Thus, the test is unable to
         * confirm the interrupt for all remaining cylinders.  ZRPB-E fixes both
         * bugs by initializing INTFLG before firing up SEEK/AIE, and also by
         * using a more correct "MOVB @#RPDS, @#RPDS", instead of the BIC. */
    } else {
        if (cyl != uptr->CYL  ||  head != uptr->HEAD) {
            assert(func != RPCS_RD_NOSEEK  &&  func != RPCS_WR_NOSEEK);
            uptr->STATUS |= RPDS_SEEK;                  /* drive is seeking */
        }
        i += RP_ROT_12;                                 /* I/O takes longer */
        assert(i);
        /* XXDP ZRPF-B / ZRPB-E have two data race conditions in Test 5 (data
         * reliability), which in ZRPB-E can be worked around with the following
         * multiplier for all 15 steady patterns, but it does not help eliminate
         * the second race in the last (random) pattern test, despite showing no
         * actual data discrepancies. */
#if 0
        if (func == RPCS_READ)
            i *= 64;                                    /* to use w/XXDP tests */
#endif
    }
    sim_activate(uptr, i);                              /* schedule */

    assert(suca == uptr->CYL);                          /* actually */
    uptr->FUNC = func;                                  /* save new func */
    uptr->HEAD = head;                                  /* save head too */
    uptr->CYL = cyl;                                    /* put on cylinder */
    suca += n >> 2;                                     /* show motion */
    return;
}

/* Complete seek */

static void rr_seek_done (UNIT *uptr, t_bool cancel)
{
    int32 n = (int32)(uptr - rr_dev.units);             /* get unit number */

    if (n == GET_DRIVE(rpcs))
        suca = cancel ? 0 : uptr->CYL;                  /* update cyl shown */
    if (uptr->SEEKING) {                                /* was seek cmd pending? */
        assert((1 << n) & RPDS_ATTN);
        rpds |= 1 << n;                                 /* set attention */
        if (rpcs & RPCS_AIE) {                          /* att ints enabled? */
            sim_debug(RRDEB_INT, &rr_dev, "rr_seek_done(SET_INT)\n");
            SET_INT(RR);
        }
        uptr->SEEKING = 0;                              /* seek completed */
    }
    uptr->STATUS &= ~RPDS_SEEK;                         /* no longer seeking */
    return;
}

/* Service a unit

   If seek in progress, complete seek command
   Then if I/O pending, complete data transfer command

   The unit control block contains the function and disk address for
   the current command.

   Some registers must be revalidated because they could have been
   modified in between go() and now.
*/

static t_stat rr_svc (UNIT *uptr)
{
    int32 n, cyl, head, sect, da, wc;
    int16 func = uptr->FUNC;
    t_seccnt todo, done;
    t_stat ioerr;
    uint32 ma;
    t_bool wr;

    assert(func);
    uptr->FUNC = 0;                                     /* idle */

    rr_seek_done(uptr, 0);                              /* complete seek, if any */
    assert(0 < func  &&  func < sizeof(rp_funcs)/sizeof(rp_funcs[0]));
    assert(!uptr->SEEKING  &&  !(uptr->STATUS & RPDS_SEEK));
    if (func == RPCS_HOME  ||  func == RPCS_SEEK)
        return SCPE_OK;                                 /* all done with seeks! */

    assert(~(rpcs & CSR_DONE));

    if (!(uptr->flags & UNIT_ATT)) {                    /* not attached? */
        rr_set_done(RPER_PGE);                          /* unit offline */
        return SCPE_UNATT;
    }
    if (uptr->STATUS & RPDS_UNSAFE) {                   /* file unsafe? */
        rr_set_done(RPER_FUV);                          /* unsafe violation */
        return SCPE_OK;
    }

    wr = func == RPCS_WRITE  ||  func == RPCS_WR_NOSEEK;

    n = (int32)(uptr - rr_dev.units);                   /* get drive no */

    cyl  = uptr->CYL;
    if (wr                                              /* write and ... */
        &&  (((wloa & RPWLOA_ON)  &&
              GET_WLOA(n, cyl) <= (wloa & RPWLOA_IMP))  /* DA write-protected? */
             ||  (uptr->flags & UNIT_WPRT))) {          /* unit write-locked? */
        rper |= RPER_WPV;
    }
    head = uptr->HEAD;
    sect = GET_SECT(rpda);                              /* get sect */
    if (sect >= RP_NUMSC)                               /* sect out of range? */
        rper |= RPER_NXS;

    if (rper) {                                         /* control in error? */
        rr_set_done(0);
        return SCPE_OK;
    }
    /* rper == 0: drive remained selected */
    assert(n == GET_DRIVE(rpcs));

    wc = 0200000 - rpwc;                                /* get wd cnt */
    assert(wc <= RP_MAXFR);
    n = GET_DTYPE(uptr->flags);                         /* get drive type */
    assert(cyl < drv_tab[n].cyl  &&  head < RP_NUMSF);
    da = GET_DA(cyl, head, sect);                       /* form full disk addr */
    assert(da < drv_tab[n].size);
    n = drv_tab[n].size - da;                           /* sectors available */

    if (rpcs & RPCS_HDR) {                              /* header ops? */
        if (!(rpcs & RPCS_MODE))                        /* yes: 18b mode? */
            rper |= RPER_MODE;                          /* must be in 18b mode */
        else if ((!wr  &&  wc != 3)  ||  (wr  &&  wc % 3)) /* DEC-11-HRPCA-C-D 3.8 */
            rper |= RPER_PGE;
        else if (wr)
            n *= 3;                                     /* 3 wds of HDR per sector */
        else
            n  = 3;                                     /* can only read 3 wds of HDR */
    } else {                                            /* no: regular R/W */
        /* RP11 can actually handle the PDP-10/-15 (18b) mode on PDP-11 using 3
         * words per transfer, combined as two 18b words on a disk sector (for
         * bit assignments in the triplet, see below BR<35:0> in format reading).
         * However, a sector written in 18b mode is marked as such and cannot be
         * read back in the PDP-11 (16b) mode (but only in the 18b mode).  Since
         * the disk containers do not have sector format information, this mode
         * cannot be supported (and for all intents and purposes it's not needed
         * for any real PDP-11 I/O).  XXDP has some test for this, though. */
        if (rpcs & RPCS_MODE)
            rper |= RPER_MODE;                          /* must be in PDP-11 mode */
#if 0   /* per doc, wc must be even; but DOS/Batch uses odd wc xfers (?!) */
        else if (wc & 1)                                /* must be even */
            rper |= RPER_PGE;
#endif
        else
            n = RP_SIZE(n);                             /* can do this many wrds */
    }

    if (rper) {                                         /* any new errors? */
        rr_set_done(0);                                 /* set done (w/errors) */
        return SCPE_OK;
    }
    if (wc > n)
        wc = n;                                         /* trim word count */
    assert(wc);

    /* A note on error handling:
     * RP11 processes data words between drive and memory absolutely sequentially
     * (not by the sector like the SIMH API provides).  Therefore, controller
     * errors should be asserted to follow this scenario.
     *
     * 1.  When reading from disk, an I/O error must be deferred until all words
     * (read from the disk so far) have been verified not to cause NXM.  If any
     * word did, then NXM gets reported, and the I/O error gets discarded
     * (because the real controller would have stopped the operation right then
     * and there, and would not have encountered the (later) I/O condition).
     *
     * 2.  When writing, I/O errors take precedence, provided that words keep
     * passing the address check on extraction from memory.  But no NXM should
     * be reported for any words that reside after the completed I/O boundary in
     * case of a short write to disk.
     *
     * 3.  Disk pack overrun is strictly a run-off of an otherwise successful
     * completion, which has left a residual word counter non-zero, because had
     * an earlier error stopped the disk operation, the overrun situation could
     * not have been experienced (the end of pack would not have been reached).
     */

    ma = ((rpcs & RPCS_MEX) << (16 - RPCS_V_MEX)) | rpba; /* get mem addr */

    if (!wr) {                                          /* read: */
        if (rpcs & RPCS_HDR) {                          /* format? */
            assert(wc <= 3);
            /* Sector header is loaded in the 36-bit Buffer Register(BR):
               17 0-bits, 9-bit cyl, 5-bit track, a spare bit, 4-bit sect;
               which is then split into 3 PDP-11 (16b) words as following */
            rpxb[0] = 0;                                /* BR<35:20> */
            rpxb[1] = (cyl << 6) | (head << 1);         /* BR<19:04> */
            rpxb[2] = sect;                             /* BR<03:00> */
            ioerr = 0;
            done = 1;                                   /* 1 sector done */
        } else {                                        /* normal read: */
            DEVICE* dptr = find_dev_from_unit(uptr);
            todo = (wc + (RP_NUMWD - 1)) / RP_NUMWD;    /* sectors to read */
            ioerr = sim_disk_rdsect(uptr, da, (uint8*) rpxb, &done, todo);
            n = RP_SIZE(done);                          /* words read */
            sim_disk_data_trace(uptr, (uint8*) rpxb, da, n * sizeof(*rpxb), "rr_read",
                                RRDEB_DAT & (dptr->dctrl | uptr->dctrl), RRDEB_OPS);
            assert(done <= todo);
            if (done >= todo)                           /* NB: done == todo */
                ioerr = 0;                              /* good stuff */
            else if (ioerr)
                wc = n;                                 /* short, adj wd cnt */
            else {
                todo -= done;                           /* to clear... */
                todo *= RP_SIZE(sizeof(*rpxb));         /* ...bytes */
                memset(rpxb + n, 0, todo);
            }
        }
        if (func == RPCS_WCHK) {
            uint32 a = ma;
            for (n = 0;  n < wc;  ++n) {                /* loop thru buf */
                RPCONTR data;
                if (MAP_RDW(a, 1, &data)) {             /* mem wd */
                    rper |= RPER_NXM;                   /* NXM? set flg */
                    wc = n;                             /* adj wd cnt */
                    break;
                }
                a += 2;
                if (rper | ioerr)
                    continue;
                if (data != rpxb[n])                    /* match to disk? */
                    rper |= RPER_WCE;                   /* no, err */
            }
        } else if ((n = MAP_WRW(ma, wc, rpxb))) {       /* store buf */
            rper |= RPER_NXM;                           /* NXM? set flag */
            wc -= n;                                    /* adj wd cnt */
        }
        if (!rper  &&  ioerr) {                         /* all wrds ok but I/O? */
            rper |= RPER_FMTE;                          /* report as FMTE */
            uptr->STATUS |= RPDS_HNF;                   /* sector not found */
            if (func == RPCS_WCHK)
                rper |= RPER_WCE;                       /* write-check err, too */
        }
        if (rper)                                       /* adj sector count */
            done = (wc + (RP_NUMWD - 1)) / RP_NUMWD;    /* NB: works for HDR, too */
    } else {                                            /* write: */
        if ((n = MAP_RDW(ma, wc, rpxb)))                /* get buf */
            wc -= n;                                    /* adj wd cnt */
        if (wc  &&  !(rpcs & RPCS_HDR)) {               /* regular write? */
            DEVICE* dptr = find_dev_from_unit(uptr);
            int32 m = (wc + (RP_NUMWD - 1)) & ~(RP_NUMWD - 1); /* clr to... */
            assert(m <= RP_MAXFR);
            memset(rpxb + wc, 0, (m - wc) * sizeof(*rpxb)); /* ...end of sect */
            sim_disk_data_trace(uptr, (uint8*) rpxb, da, m * sizeof(*rpxb), "rr_write",
                                RRDEB_DAT & (dptr->dctrl | uptr->dctrl), RRDEB_OPS);
            todo = m / RP_NUMWD;                        /* sectors to write */
            assert(!(m % RP_NUMWD));
            ioerr = sim_disk_wrsect(uptr, da, (uint8*) rpxb, &done, todo);
            assert(done <= todo);
            if (done < todo) {                          /* short write? */
                wc = RP_SIZE(done);                     /* words written */
                rper |= RPER_FMTE;                      /* report as FMTE */
                uptr->STATUS |= RPDS_HNF;               /* sector not found */
                if (!ioerr)
                    ioerr = 1;                          /* must be reported! */
            } else if (n) {
                rper |= RPER_NXM;                       /* NXM? set flg */
                ioerr = 0;                              /* don't care */
            } else
                ioerr = 0;                              /* good stuff */
        } else {
            ioerr = 0;                                  /* good stuff */
            done = (wc + 2) / 3;
            if (n)
                rper |= RPER_NXM;                       /* NXM? set flg */
        }
    }
    assert(!ioerr  ||  rper);
    assert(!wc  ||  done);
    assert(wc  ||  rper);

    rpwc += wc;
    rpwc &= RPWC_IMP;
    ma   += wc << 1;
    rpba  = ma & RPBA_IMP;
    rpcs &= ~RPCS_MEX;
    rpcs |= (ma >> (16 - RPCS_V_MEX)) & RPCS_MEX;
    if (rpwc  &&  !rper)
        rper |= RPER_EOP;                               /* disk pack overrun */

    da += done ? done : 1;                              /* update DA */
    n = GET_DTYPE(uptr->flags);                         /* drive type */
    assert(da <= drv_tab[n].size);
    sect = da % RP_NUMSC;                               /* new sector */
    head = da / RP_NUMSC;                               /* new head (w/cyl) */
    todo = head / RP_NUMSF;                             /* new cyl (tentative) */
    if (todo == drv_tab[n].cyl) {                       /* at the end? */
        cyl   = drv_tab[n].cyl - 1;                     /* keep on last cyl */
        todo  = 0;                                      /* wrap cyl for rpda... */
        head  = 0;                                      /* ...and head, too */
        assert(!sect);
    } else {
        cyl   = todo;                                   /* new cyl */
        head %= RP_NUMSF;                               /* isolate head */
    }
    if ((func == RPCS_RD_NOSEEK  ||  func == RPCS_WR_NOSEEK) /* no SEEK I/O... */
        &&  (uptr->CYL != cyl                           /* ...and: arm moved or... */
             ||  (rper & RPER_EOP))) {                  /* ...boundary exceeded? */
        n = (int32)(uptr - rr_dev.units);               /* get unit number */
        assert((1 << n) & RPDS_ATTN);
        rpds |= 1 << n;                                 /* set attention */
        if (rpcs & RPCS_AIE) {                          /* att ints enabled? */
            sim_debug(RRDEB_INT, &rr_dev, "rr_svc(SET_INT)\n");
            SET_INT(RR);                                /* request interrupt */
        }
    }
    uptr->HEAD = head;                                  /* update head */
    uptr->CYL = cyl;                                    /* update cyl */
    rpda = (head << RPDA_V_TRACK) | sect;               /* updated head / sect */
    rpca = todo;                                        /* wrapped up cyl */
    suca = cyl;                                         /* updated real cyl */

    rr_set_done(0);                                     /* all done here */

    if (ioerr) {                                        /* I/O error? */
        const char* name = uptr->uname;
        const char* file = uptr->filename;
        const char* errstr = errno ? strerror(errno) : "";
        sim_printf("RR%u %s [%s:%s] FUNC=%s(%c) RPER=%06o I/O error (%s)%s%s",
                   (int) GET_DRIVE(rpcs), GET_DTYPE(uptr->flags) ? RP_RP03 : RP_RP02,
                   name ? name : "???", file ? file : "<NULL>",
                   rp_funcs[func], "WR"[!wr], (int) rper, sim_error_text(ioerr),
                   errstr  &&  *errstr ? ": " : "", errstr ? errstr : "");
        return SCPE_IOERR;
    }
    return SCPE_OK;
}

/* Interrupt state change routines

   rr_clr_done          clear done
   rr_set_done          set done (and errors)
   rr_inta              interrupt acknowledge
*/

static void rr_clr_done (void)
{
    assert(rpcs & CSR_DONE);
    rpcs &= ~CSR_DONE;                                  /* clear done */
    if ((rpcs & CSR_IE)  &&  (!(rpcs & RPCS_AIE)  ||  !(rpds & RPDS_ATTN))) {
        sim_debug(RRDEB_INT, &rr_dev, "rr_clr_done(CLR_INT)\n");
        CLR_INT(RR);                                    /* clear int req */
    }
    return;
}

static void rr_set_done (int32 err)
{
    assert(~(rpcs & CSR_DONE));
    rper |= err;
    rpcs |= CSR_DONE;                                   /* set done */
    if (rpcs & CSR_IE) {                                /* int enable? */
        sim_debug(RRDEB_INT, &rr_dev, "rr_set_done(SET_INT)\n");
        SET_INT(RR);                                    /* request int */
    }
    return;
}

static int32 rr_inta (void)
{
    sim_debug(RRDEB_INT, &rr_dev, "rr_inta()\n");
    assert(((rpcs & RPCS_AIE)  &&  (rpds & RPDS_ATTN))  ||
            ((rpcs & CSR_IE)  &&  (rpcs & CSR_DONE)));
    rpcs &= ~RPCS_AIE;                                  /* AIE is one-shot */
    return rr_dib.vec;                                  /* return vector */
}

/* Device reset */

static t_stat rr_reset (DEVICE *dptr)
{
    int32 i;

    /* compile-time sanity check first */
    const_assert(sizeof(rr_regs)/sizeof(rr_regs[0]) == RP_IOLN/2 - RP_IOFF);

    assert(dptr == &rr_dev);

    /* clear everything now */
    rpds = 0;
    rper = 0;
    rpcs = CSR_DONE;
    rpwc = 0;
    rpba = 0;
    rpca = 0;
    rpda = 0;
    suca = 0;
    for (i = 0;  i < RP_NUMDR;  ++i) {
        UNIT* uptr = rr_dev.units + i;
        sim_cancel(uptr);
        uptr->action = rr_svc;
        uptr->SEEKING = 0;
        uptr->STATUS = 0;
        uptr->FUNC = 0;
        uptr->HEAD = 0;
        uptr->CYL = 0;
    }
    assert(dptr == &rr_dev);
    sim_debug(RRDEB_INT, dptr, "rr_reset(CLR_INT)\n");
    CLR_INT(RR);
    if (rpxb == NULL)
        rpxb  = (RPCONTR*) calloc(RP_MAXFR, sizeof(*rpxb));
    if (rpxb == NULL)
        return SCPE_MEM;
    return auto_config(NULL, 0);
}

/* Attach/detach routines */

static t_stat rr_attach (UNIT *uptr, CONST char *cptr)
{
    static const char* rr_types[] = { RP_RP03, RP_RP02, NULL };
    int32 type = GET_DTYPE(uptr->flags);
    t_stat err = sim_disk_attach_ex2(uptr, cptr,
                                     RP_SIZE(sizeof(*rpxb)), sizeof(*rpxb),
                                     TRUE, 0, drv_tab[type].name,
                                     0, 0,
                                     rr_dev.flags & DEV_RP11CE ? rr_types : NULL,
                                     0);
    if (err == SCPE_OK  &&  !(uptr->STATUS & RPDS_DKER))
        uptr->STATUS &= ~RPDS_UNSAFE;
    return err;
}

static t_stat rr_detach (UNIT *uptr)
{
    int16 func;
    sim_cancel(uptr);
    func = uptr->FUNC;
    rr_seek_done(uptr, 1/*cancel*/);
    if (func) {                                         /* anything unfinished? */
        uptr->FUNC = 0;                                 /* idle now */
        if (func == RPCS_SEEK)
            uptr->STATUS |= RPDS_INC;                   /* seek incomplete */
        else if (func != RPCS_HOME) {
            uptr->STATUS |= RPDS_HNF;                   /* sector not found */
            rr_set_done(RPER_TE);                       /* ...and timing error */
        }
        uptr->STATUS |= RPDS_UNSAFE;                    /* must reset before use */
    }
    assert(!sim_is_active(uptr));
    assert(!uptr->SEEKING);
    uptr->action = rr_svc;
    uptr->HEAD = 0;
    uptr->CYL = 0;
    return sim_disk_detach(uptr);
}

/* Set / show drive type */

static t_stat rr_set_type (UNIT *uptr, int32 val, CONST char *cptr, void *desc)
{
    assert(find_dev_from_unit(uptr) == &rr_dev);
    if ((val & ~UNIT_RP03)  ||  cptr)
        return SCPE_ARG;
    if (uptr->flags & UNIT_ATT)
        return SCPE_ALATT;
    if (val  &&  !(rr_dev.flags & DEV_RP11CE))
        return SCPE_ARG;
    assert(!(uptr->CYL | uptr->HEAD));
    uptr->capac  = RP_SIZE(drv_tab[GET_DTYPE(val)].size);
    uptr->flags &= ~UNIT_RP03;
    uptr->flags |= val;
    return SCPE_OK;
}

static t_stat rr_show_type (FILE *st, UNIT *uptr, int32 val, CONST void *desc)
{
    fputs(drv_tab[GET_DTYPE(uptr->flags)].name, st);
    return SCPE_OK;
}

/* Set WLOA */

static t_stat rr_set_wloa (UNIT *uptr, int32 val, CONST char *cptr, void *desc)
{
    assert(find_dev_from_unit(uptr) == &rr_dev);
    if (!cptr  ||  !*cptr)
        return SCPE_2FARG;
    if (strncasecmp(cptr, "OFF", 3) == 0) {
        cptr += 3;
        if (*cptr == ';')
            return SCPE_2MARG;
        if (*cptr)
            return SCPE_ARG;
        wloa &= ~RPWLOA_ON;
        return SCPE_OK;
    }
    if (strncasecmp(cptr, "ON", 2) != 0)
        return SCPE_ARG;
    cptr += 2;
    if (*cptr == ';') {
        char* end;
        long val;
        if (!*++cptr)
            return SCPE_MISVAL;
        errno = 0;
        val = strtol(cptr, &end, 0);
        if (errno  ||  !end  ||  *end  ||  end == cptr  ||  (val & ~RPWLOA_IMP))
            return SCPE_ARG;
        wloa &= ~RPWLOA_IMP;
        wloa |= val;
    } else if (*cptr)
        return SCPE_ARG;
    wloa |= RPWLOA_ON;
    return SCPE_OK;
}

/* Device bootstrap */

#define BOOT_START      02000                           /* start */
#define BOOT_ENTRY      (BOOT_START + 002)              /* entry */
#define BOOT_UNIT       (BOOT_START + 010)              /* unit number */
#define BOOT_CSR        (BOOT_START + 014)              /* CSR + 12 */
#define BOOT_LEN        (sizeof(rr_boot_rom)/sizeof(rr_boot_rom[0]))

static const uint16 rr_boot_rom[] = {
/* EXPECTED M9312 REGISTER USE FOR BOOT PROMS (IN THE BOOTED SOFTWARE):                                *
 * R0     = UNIT NUMBER                                                                                *
 * R1     = CONTROLLER CSR                                                                             *
 * R2, R3 = TEMPORARIES                                                                                *
 * R4     = ALWAYS POINTS TO PROM BASE + 20 (HELPS LOCATE THE BOOTED DEVICE DESIGNATION)               *
 * R5     = LAST COMMAND DATA (E.G. LOAD ADDR, EXAM DATA; OTHERWISE, JUNK)                             *
 * R6(SP) = PC OF THE COMMAND START (CONTAINS THE ADDRESS WHERE THE BOOT COMMAND ORIGINATED FROM)      */
/*                                              .TITLE RP11 BOOT M9312 STYLE - TONY LAWRENCE (C) 2023  */
/*                                              .ASECT                                                 */
/* 002000                                       .=2000                                                 */
/* 002000 */ 0042120,                 /* START: .WORD   "PD             ; "DP" (RP DEVICE DESIGNATION) */
/* 002002 */ 0012706, BOOT_ENTRY,     /* BOOT:  MOV     #BOOT, SP       ; ENTRY POINT PC               */
/* 002006 */ 0112700, 0000000,        /*        MOVB    #0, R0          ; UNIT NUMBER                  */
/* 002012 */ 0012701, 0176726,        /*        MOV     #176726, R1     ; RPCS + 12                    */
/* 002016 */ 0012704, BOOT_START+020, /*        MOV     #<START+20>, R4 ; BACKLINK TO PROM W/OFFSET 20 */
/* 002022 */ 0005041,                 /*        CLR     -(R1)           ; DISK ADDRESS                 */
/* 002024 */ 0005041,                 /*        CLR     -(R1)           ; CYLINDER ADDRESS             */
/* 002026 */ 0005041,                 /*        CLR     -(R1)           ; MEMORY ADDRESS               */
/* 002030 */ 0012741, 0177000,        /*        MOV     #-512., -(R1)   ; WORD CT (ALWAYS 2 FULL SECTS)*/
/* 002034 */ 0010003,                 /*        MOV     R0, R3                                         */
/* 002036 */ 0000303,                 /*        SWAB    R3              ; MOVE UNIT# INTO POSITION     */
/* 002040 */ 0052703, 0000005,        /*        BIS     #5, R3          ; COMBINE READ+GO FUNCTION     */
/* 002044 */ 0010341,                 /*        MOV     R3, -(R1)       ; DO IT!                       */
/* 002046 */ 0005005,                 /*        CLR     R5              ; M9312 USES FOR DISPLAY       */
/* 002050 */ 0105711,                 /* 1$:    TSTB    (R1)            ; READY?                       */
/* 002052 */ 0100376,                 /*        BPL     1$              ; BR IF NOT                    */
/* 002054 */ 0005711,                 /*        TST     (R1)            ; ERROR?                       */
/* 002056 */ 0100002,                 /*        BPL     2$              ; BR IF NOT                    */
/* 002060 */ 0000005,                 /*        RESET                                                  */
/* 002062 */ 0000747,                 /*        BR      BOOT            ; START OVER                   */
/* 002064 */ 0105011,                 /* 2$:    CLRB    (R1)            ; CLEAR CONTROLLER             */
/* 002066 */ 0005007                  /*        CLR     PC              ; JUMP TO BOOTSTRAP            */
/* 002070                                       .END                                                   */
};

static t_stat rr_boot (int32 unitno, DEVICE *dptr)
{
    size_t i;
    assert(dptr == &rr_dev);
    for (i = 0;  i < BOOT_LEN;  ++i)
        WrMemW(BOOT_START + (2 * i), rr_boot_rom[i]);
    WrMemW(BOOT_UNIT, unitno & (RP_NUMDR - 1));
    WrMemW(BOOT_CSR, (rr_dib.ba & DMASK) + (014/*CSR*/ + 012));
    cpu_set_boot(BOOT_ENTRY);
    return SCPE_OK;
}

/* Misc */

static t_stat rr_help (FILE *st, DEVICE *dptr, UNIT *uptr, int32 flag, const char *cptr)
{
    size_t i, n;
    assert(dptr == &rr_dev);
    fputs(
    /*567901234567890123456789012345678901234567890123456789012345678901234567890*/
    RP_RP11 "/" RP_RP11CE "/" RP_RP02 "/" RP_RP03 " disk pack device\n\n"
    "A detailed description of this device can be found in the\n"
    "\"PDP-11 Peripherals Handbook\" (1973 - 1976) and in the technical manual\n"
    "\"RP11-C Disk Pack Drive Controller Maintenance Manual\" (1974)\n"
    "(DEC-11-HRPCA-C-D).\n\n"
    "In default configuration " RP_RP11 " responds to the range 17776700 - 17776736\n"
    "with the first 4 word locations not occupied by any device registers (and\n"
    "so 17776710 is the first used location).  Some operating systems want you\n"
    "to specify the extended range (e.g. RSTS), but some -- the relevant range\n"
    "(17776710 - 17776736), yet some just want to know where the CSR is located\n"
    "(17776714 by default), so they can auto-calculate the range on their own.\n\n"
    "Disk drive parameters (all decimal):\n\n"
    "Type    Cylinders    Heads  Sects/Trk     Capacity    Average access\n"
    "      Total   Spare                   Nominal  Usable    time, ms\n", st);
    n = dptr->flags & DEV_RP11CE ? sizeof(drv_tab)/sizeof(drv_tab[0]) : 1;
    for (i = 0;  i < n;  ++i) {
        uint32 spare = GET_DA(drv_tab[i].spare, RP_NUMSF, RP_NUMSC);
        uint32 total = drv_tab[i].size;
        fprintf(st, "%.6s: %5u   %5u  %5u  %5u"
                "    %5.1fMB  %5.1fMB   %5u.%1u\n", drv_tab[i].name,
                drv_tab[i].cyl, drv_tab[i].spare, RP_NUMSF, RP_NUMSC,
                RP_SIZE(total - spare) / .5e6, RP_SIZE(total) / .5e6,
                (drv_tab[i].seek_ave + RP_ROT_12)/10,
                (drv_tab[i].seek_ave + RP_ROT_12)%10);
    }
    fputs("\n"
    "The implementation does not include any maintenance registers or disk/sector\n"
    "formatting operations yet supports the Write Lockout Address (LOA) register.\n"
    "It can be set with the PROTECT command using the keyword \"ON\" followed by a\n"
    "value representing the number of continuously protected drives and cylinders:\n"
    "  bits <10:8> represent the highest protected unit number;\n"
    "  bits <07:0> encode the last protected cylinder on the unit number specified\n"
    "above (all units preceding that unit become fully write-protected).\n"
    "For example,\n\n"
    "    sim> set RR PROTECT=ON;0407\n\n", st);
    fprintf(st,
    "write-protects the entire drive unit 0 and cylinders 0 thru 7%s\n",
            dptr->flags & DEV_RP11CE ? " x 2 + 1 = 15(10)" : "");
    fputs(
    "on drive unit 1.  The value is accepted in either decimal, octal (if it starts\n"
    "with 0), or hexadecimal (if it starts with 0x) notation (so \"0x107\" in the\n"
    "above example effects the same setting).  Alternatively, a deposit command\n"
    "can be used, but it only allows native (octal) values:\n\n"
    "    sim> deposit RR WLOA 100407\n\n"
    "The current setting can be obtained by examining the WLOA register in\n"
    "the device (the sign bit not present in hardware controls the feature):\n\n"
    "    sim> examine RR WLOA\n"
    "    WLOA:   100407  PROTECT=ON DRV=1 CYL=7\n\n"
    "To remove the lockout:\n\n"
    "    sim> set RR PROTECT=OFF\n"
    "    sim> examine RR WLOA\n"
    "    WLOA:   000407  PROTECT=OFF DRV=1 CYL=7\n\n"
    "Note that it does not clear the address but turns the feature off.\n"
    "The WLOA register is unaffected by the device RESET.\n", st);
    fprint_set_help (st, dptr);
    fprint_show_help(st, dptr);
    fprintf(st,
    "\nThe " RP_RP11 " is disabled in a Qbus system with more than 256KB of memory.\n");
    fprint_reg_help (st, dptr);
    return SCPE_OK;
}

static const char *rr_description (DEVICE *dptr)
{
    assert(dptr == &rr_dev);
    return dptr->flags & DEV_RP11CE
        ? RP_RP11CE "/" RP_RP02 "/" RP_RP03 " disk pack device"
        : RP_RP11 "/" RP_RP02 " disk pack device";
}

/* Set / show controller type */

static t_stat rr_set_ctrl (UNIT *uptr, int32 val, CONST char *cptr, void *desc)
{
    size_t i;
    assert(find_dev_from_unit(uptr) == &rr_dev);
    if ((val & ~DEV_RP11CE)  ||  cptr)
        return SCPE_ARG;
    if (!val) {                                         /* going RP11? */
        for (i = 0;  i < RP_NUMDR;  ++i) {
            uptr = rr_dev.units + i;
            if ((uptr->flags & UNIT_ATT)  &&  (uptr->flags & UNIT_RP03))
                return SCPE_ALATT;                      /* no RP03 allowed */
        }
    }
    rr_dev.flags &= ~DEV_RP11CE;
    rr_dev.flags |= val;
    for (i = 0;  i < RP_NUMDR;  ++i) {
        uptr = rr_dev.units + i;
        if (uptr->flags & UNIT_ATT)
            continue;
        if (rr_set_type(uptr, val ? UNIT_RP03 : 0, 0, 0) != SCPE_OK)
            return SCPE_IERR;
    }
    return SCPE_OK;
}

static t_stat rr_show_ctrl (FILE *st, UNIT *uptr, int32 val, CONST void *desc)
{
    assert(find_dev_from_unit(uptr) == &rr_dev);
    fputs(rr_dev.flags & DEV_RP11CE ? RP_RP11CE : RP_RP11, st);
    return SCPE_OK;
}

#elif !defined(UC15)
#  error "RP11/-C/-E can only be used in PDP-11 configuration"
#endif /*VM_PDP11 && !UC15*/
