| 1 | /* ibm1130_disk.c: IBM 1130 disk IO simulator\r |
| 2 | \r |
| 3 | NOTE - there is a problem with this code. The Device Status Word (DSW) is\r |
| 4 | computed from current conditions when requested by an XIO load status\r |
| 5 | command; the value of DSW available to the simulator's examine & save\r |
| 6 | commands may NOT be accurate. This should probably be fixed.\r |
| 7 | \r |
| 8 | Based on the SIMH package written by Robert M Supnik\r |
| 9 | \r |
| 10 | * (C) Copyright 2002, Brian Knittel.\r |
| 11 | * You may freely use this program, but: it offered strictly on an AS-IS, AT YOUR OWN\r |
| 12 | * RISK basis, there is no warranty of fitness for any purpose, and the rest of the\r |
| 13 | * usual yada-yada. Please keep this notice and the copyright in any distributions\r |
| 14 | * or modifications.\r |
| 15 | *\r |
| 16 | * Revision History\r |
| 17 | * 05-dec-06 Added cgiwritable mode\r |
| 18 | *\r |
| 19 | * 19-Dec-05 We no longer issue an operation complete interrupt if an INITR, INITW\r |
| 20 | * or CONTROL operation is attemped on a drive that is not online. DATA_ERROR\r |
| 21 | * is now only indicated in the DSW when \r |
| 22 | *\r |
| 23 | * 02-Nov-04 Addes -s option to boot to leave switches alone.\r |
| 24 | * 15-jun-03 moved actual read on XIO read to end of time interval,\r |
| 25 | * as the APL boot card required 2 instructions to run between the\r |
| 26 | * time read was initiated and the time the data was read (a jump and a wait)\r |
| 27 | *\r |
| 28 | * 01-sep-02 corrected treatment of -m and -r flags in dsk_attach\r |
| 29 | * in cgi mode, so that file is opened readonly but emulated\r |
| 30 | * disk is writable.\r |
| 31 | *\r |
| 32 | * This is not a supported product, but I welcome bug reports and fixes.\r |
| 33 | * Mail to simh@ibm1130.org\r |
| 34 | */\r |
| 35 | \r |
| 36 | #include "ibm1130_defs.h"\r |
| 37 | #include "memory.h"\r |
| 38 | \r |
| 39 | #define TRACE_DMS_IO /* define to enable debug of DMS phase IO */\r |
| 40 | \r |
| 41 | #ifdef TRACE_DMS_IO\r |
| 42 | extern int32 sim_switches;\r |
| 43 | extern int32 sim_quiet;\r |
| 44 | static int trace_dms = 0;\r |
| 45 | static void tracesector (int iswrite, int nwords, int addr, int sector);\r |
| 46 | static t_stat where_cmd (int flag, char *ptr);\r |
| 47 | static t_stat phdebug_cmd (int flag, char *ptr);\r |
| 48 | static t_stat fdump_cmd (int flags, char *cptr);\r |
| 49 | static void enable_dms_tracing (int newsetting);\r |
| 50 | #endif\r |
| 51 | \r |
| 52 | /* Constants */\r |
| 53 | \r |
| 54 | #define DSK_NUMWD 321 /* words/sector */\r |
| 55 | #define DSK_NUMSC 4 /* sectors/surface */\r |
| 56 | #define DSK_NUMSF 2 /* surfaces/cylinder */\r |
| 57 | #define DSK_NUMCY 203 /* cylinders/drive */\r |
| 58 | #define DSK_NUMTR (DSK_NUMCY * DSK_NUMSF) /* tracks/drive */\r |
| 59 | #define DSK_NUMDR 5 /* drives/controller */\r |
| 60 | #define DSK_SIZE (DSK_NUMCY * DSK_NUMSF * DSK_NUMSC * DSK_NUMWD) /* words/drive */\r |
| 61 | \r |
| 62 | #define UNIT_V_RONLY (UNIT_V_UF + 0) /* hwre write lock */\r |
| 63 | #define UNIT_V_OPERR (UNIT_V_UF + 1) /* operation error flag */\r |
| 64 | #define UNIT_V_HARDERR (UNIT_V_UF + 2) /* hard error flag (reset on power down) */\r |
| 65 | #define UNIT_RONLY (1u << UNIT_V_RONLY)\r |
| 66 | #define UNIT_OPERR (1u << UNIT_V_OPERR)\r |
| 67 | #define UNIT_HARDERR (1u << UNIT_V_HARDERR)\r |
| 68 | \r |
| 69 | #define MEM_MAPPED(uptr) (uptr->flags & UNIT_BUF) /* disk buffered in memory */\r |
| 70 | \r |
| 71 | #define IO_NONE 0 /* last operation, used to ensure fseek between read and write */\r |
| 72 | #define IO_READ 1\r |
| 73 | #define IO_WRITE 2\r |
| 74 | \r |
| 75 | #define DSK_DSW_DATA_ERROR 0x8000 /* device status word bits */\r |
| 76 | #define DSK_DSW_OP_COMPLETE 0x4000\r |
| 77 | #define DSK_DSW_NOT_READY 0x2000\r |
| 78 | #define DSK_DSW_DISK_BUSY 0x1000\r |
| 79 | #define DSK_DSW_CARRIAGE_HOME 0x0800\r |
| 80 | #define DSK_DSW_SECTOR_MASK 0x0003\r |
| 81 | \r |
| 82 | /* device status words */\r |
| 83 | static int16 dsk_dsw[DSK_NUMDR] = {DSK_DSW_NOT_READY, DSK_DSW_NOT_READY, DSK_DSW_NOT_READY, DSK_DSW_NOT_READY, DSK_DSW_NOT_READY};\r |
| 84 | static int16 dsk_sec[DSK_NUMDR] = {0}; /* next-sector-up */\r |
| 85 | static char dsk_lastio[DSK_NUMDR]; /* last stdio operation: IO_READ or IO_WRITE */\r |
| 86 | int32 dsk_swait = 50; /* seek time -- see how short a delay we can get away with */\r |
| 87 | int32 dsk_rwait = 50; /* rotate time */\r |
| 88 | static t_bool raw_disk_debug = FALSE;\r |
| 89 | \r |
| 90 | static t_stat dsk_svc (UNIT *uptr);\r |
| 91 | static t_stat dsk_reset (DEVICE *dptr);\r |
| 92 | static t_stat dsk_attach (UNIT *uptr, char *cptr);\r |
| 93 | static t_stat dsk_detach (UNIT *uptr);\r |
| 94 | static t_stat dsk_boot (int unitno, DEVICE *dptr);\r |
| 95 | \r |
| 96 | static void diskfail (UNIT *uptr, int dswflag, int unitflag, t_bool do_interrupt);\r |
| 97 | \r |
| 98 | /* DSK data structures\r |
| 99 | \r |
| 100 | dsk_dev disk device descriptor\r |
| 101 | dsk_unit unit descriptor\r |
| 102 | dsk_reg register list\r |
| 103 | */\r |
| 104 | \r |
| 105 | UNIT dsk_unit[] = {\r |
| 106 | { UDATA (&dsk_svc, UNIT_FIX+UNIT_ATTABLE+UNIT_DISABLE, DSK_SIZE) },\r |
| 107 | { UDATA (&dsk_svc, UNIT_FIX+UNIT_ATTABLE+UNIT_DISABLE, DSK_SIZE) },\r |
| 108 | { UDATA (&dsk_svc, UNIT_FIX+UNIT_ATTABLE+UNIT_DISABLE, DSK_SIZE) },\r |
| 109 | { UDATA (&dsk_svc, UNIT_FIX+UNIT_ATTABLE+UNIT_DISABLE, DSK_SIZE) },\r |
| 110 | { UDATA (&dsk_svc, UNIT_FIX+UNIT_ATTABLE+UNIT_DISABLE, DSK_SIZE) }\r |
| 111 | };\r |
| 112 | \r |
| 113 | #define IS_ONLINE(u) (((u)->flags & (UNIT_ATT|UNIT_DIS)) == UNIT_ATT)\r |
| 114 | \r |
| 115 | /* Parameters in the unit descriptor */\r |
| 116 | \r |
| 117 | #define CYL u3 /* current cylinder */\r |
| 118 | #define FUNC u4 /* current function */\r |
| 119 | \r |
| 120 | REG dsk_reg[] = {\r |
| 121 | { HRDATA (DSKDSW0, dsk_dsw[0], 16) },\r |
| 122 | { HRDATA (DSKDSW1, dsk_dsw[1], 16) },\r |
| 123 | { HRDATA (DSKDSW2, dsk_dsw[2], 16) },\r |
| 124 | { HRDATA (DSKDSW3, dsk_dsw[3], 16) },\r |
| 125 | { HRDATA (DSKDSW4, dsk_dsw[4], 16) },\r |
| 126 | { DRDATA (STIME, dsk_swait, 24), PV_LEFT },\r |
| 127 | { DRDATA (RTIME, dsk_rwait, 24), PV_LEFT },\r |
| 128 | { NULL } };\r |
| 129 | \r |
| 130 | MTAB dsk_mod[] = {\r |
| 131 | { UNIT_RONLY, 0, "write enabled", "ENABLED", NULL },\r |
| 132 | { UNIT_RONLY, UNIT_RONLY, "write locked", "LOCKED", NULL },\r |
| 133 | { 0 } };\r |
| 134 | \r |
| 135 | DEVICE dsk_dev = {\r |
| 136 | "DSK", dsk_unit, dsk_reg, dsk_mod,\r |
| 137 | DSK_NUMDR, 16, 16, 1, 16, 16,\r |
| 138 | NULL, NULL, &dsk_reset,\r |
| 139 | dsk_boot, dsk_attach, dsk_detach};\r |
| 140 | \r |
| 141 | static int32 dsk_ilswbit[DSK_NUMDR] = { /* interrupt level status word bits for the drives */\r |
| 142 | ILSW_2_1131_DISK,\r |
| 143 | ILSW_2_2310_DRV_1,\r |
| 144 | ILSW_2_2310_DRV_2,\r |
| 145 | ILSW_2_2310_DRV_3,\r |
| 146 | ILSW_2_2310_DRV_4,\r |
| 147 | };\r |
| 148 | \r |
| 149 | static int32 dsk_ilswlevel[DSK_NUMDR] =\r |
| 150 | {\r |
| 151 | 2, /* interrupt levels for the drives */\r |
| 152 | 2, 2, 2, 2\r |
| 153 | };\r |
| 154 | \r |
| 155 | typedef enum {DSK_FUNC_IDLE, DSK_FUNC_READ, DSK_FUNC_VERIFY, DSK_FUNC_WRITE, DSK_FUNC_SEEK, DSK_FUNC_FAILED} DSK_FUNC;\r |
| 156 | \r |
| 157 | static struct tag_dsk_action { /* stores data needed for pending IO activity */\r |
| 158 | int32 io_address;\r |
| 159 | uint32 io_filepos;\r |
| 160 | int io_nwords;\r |
| 161 | int io_sector;\r |
| 162 | } dsk_action[DSK_NUMDR];\r |
| 163 | \r |
| 164 | /* xio_disk - XIO command interpreter for the disk drives */\r |
| 165 | /*\r |
| 166 | * device status word:\r |
| 167 | *\r |
| 168 | * 0 data error, occurs when:\r |
| 169 | * 1. A modulo 4 error is detected during a read, read-check, or write operation. \r |
| 170 | * 2. The disk storage is in a read or write mode at the leading edge of a sector pulse. \r |
| 171 | * 3. A seek-incomplete signal is received from the 2311.\r |
| 172 | * 4. A write select error has occurred in the disk storage drive. \r |
| 173 | * 5. The power unsafe latch is set in the attachment.\r |
| 174 | * Conditions 1, 2, and 3 are turned off by a sense device command with modifier bit 15\r |
| 175 | * set to 1. Conditions 4 and 5 are turned off by powering the drive off and back on.\r |
| 176 | * 1 operation complete\r |
| 177 | * 2 not ready, occurs when disk not ready or busy or disabled or off-line or\r |
| 178 | * power unsafe latch set. Also included in the disk not ready is the write select error,\r |
| 179 | * which can be a result of power unsafe or write select. \r |
| 180 | * 3 disk busy\r |
| 181 | * 4 carriage home (on cyl 0)\r |
| 182 | * 15-16: number of next sector spinning into position.\r |
| 183 | */\r |
| 184 | \r |
| 185 | extern void void_backtrace (int afrom, int ato);\r |
| 186 | \r |
| 187 | void xio_disk (int32 iocc_addr, int32 func, int32 modify, int drv)\r |
| 188 | {\r |
| 189 | int i, rev, nsteps, newcyl, sec, nwords;\r |
| 190 | uint32 newpos; /* changed from t_addr to uint32 in anticipation of simh 64-bit development */\r |
| 191 | char msg[80];\r |
| 192 | UNIT *uptr = dsk_unit+drv;\r |
| 193 | int16 buf[DSK_NUMWD];\r |
| 194 | \r |
| 195 | if (! BETWEEN(drv, 0, DSK_NUMDR-1)) { /* hmmm, invalid drive */\r |
| 196 | if (func != XIO_SENSE_DEV) { /* tried to use it, too */\r |
| 197 | /* just do nothing, as if the controller isn't there. NAMCRA at N0116300 tests for drives by attempting reads\r |
| 198 | sprintf(msg, "Op %x on invalid drive number %d", func, drv);\r |
| 199 | xio_error(msg);\r |
| 200 | */\r |
| 201 | }\r |
| 202 | return;\r |
| 203 | }\r |
| 204 | \r |
| 205 | CLRBIT(uptr->flags, UNIT_OPERR); /* clear pending error flag from previous op, if any */\r |
| 206 | \r |
| 207 | switch (func) {\r |
| 208 | case XIO_INITR:\r |
| 209 | if (! IS_ONLINE(uptr)) { /* disk is offline */\r |
| 210 | diskfail(uptr, 0, 0, FALSE);\r |
| 211 | break;\r |
| 212 | }\r |
| 213 | \r |
| 214 | sim_cancel(uptr); /* cancel any pending ops */\r |
| 215 | dsk_dsw[drv] |= DSK_DSW_DISK_BUSY; /* and mark the disk as busy */\r |
| 216 | \r |
| 217 | nwords = M[iocc_addr++ & mem_mask]; /* get word count w/o upsetting SAR/SBR */\r |
| 218 | \r |
| 219 | if (nwords == 0) /* this is bad -- on real 1130, this locks up disk controller ! */\r |
| 220 | break;\r |
| 221 | \r |
| 222 | if (! BETWEEN(nwords, 1, DSK_NUMWD)) { /* count bad */\r |
| 223 | SETBIT(uptr->flags, UNIT_OPERR); /* set data error DSW bit when op complete */\r |
| 224 | nwords = DSK_NUMWD; /* limit xfer to proper sector size */\r |
| 225 | }\r |
| 226 | \r |
| 227 | sec = modify & 0x07; /* get sector on cylinder */\r |
| 228 | \r |
| 229 | if ((modify & 0x0080) == 0) { /* it's a real read if it's not a read check */\r |
| 230 | /* ah. We have a problem. The APL boot card counts on there being time for at least one\r |
| 231 | * more instruction to execute between the XIO read and the time the data starts loading\r |
| 232 | * into core. So, we have to defer the actual read operation a bit. Might as well wait\r |
| 233 | * until it's time to issue the operation complete interrupt. This means saving the\r |
| 234 | * IO information, then performing the actual read in dsk_svc.\r |
| 235 | */\r |
| 236 | \r |
| 237 | newpos = (uptr->CYL*DSK_NUMSC*DSK_NUMSF + sec)*2*DSK_NUMWD;\r |
| 238 | \r |
| 239 | dsk_action[drv].io_address = iocc_addr;\r |
| 240 | dsk_action[drv].io_nwords = nwords;\r |
| 241 | dsk_action[drv].io_sector = sec;\r |
| 242 | dsk_action[drv].io_filepos = newpos;\r |
| 243 | \r |
| 244 | uptr->FUNC = DSK_FUNC_READ;\r |
| 245 | }\r |
| 246 | else {\r |
| 247 | trace_io("* DSK%d verify %d.%d (%x)", drv, uptr->CYL, sec, uptr->CYL*8 + sec);\r |
| 248 | \r |
| 249 | if (raw_disk_debug)\r |
| 250 | printf("* DSK%d verify %d.%d (%x)", drv, uptr->CYL, sec, uptr->CYL*8 + sec);\r |
| 251 | \r |
| 252 | uptr->FUNC = DSK_FUNC_VERIFY;\r |
| 253 | }\r |
| 254 | \r |
| 255 | sim_activate(uptr, dsk_rwait);\r |
| 256 | break;\r |
| 257 | \r |
| 258 | case XIO_INITW:\r |
| 259 | if (! IS_ONLINE(uptr)) { /* disk is offline */\r |
| 260 | diskfail(uptr, 0, 0, FALSE);\r |
| 261 | break;\r |
| 262 | }\r |
| 263 | \r |
| 264 | if (uptr->flags & UNIT_RONLY) { /* oops, write to RO disk? permanent error until disk is powered off/on */\r |
| 265 | diskfail(uptr, DSK_DSW_DATA_ERROR, UNIT_HARDERR, FALSE);\r |
| 266 | break;\r |
| 267 | }\r |
| 268 | \r |
| 269 | sim_cancel(uptr); /* cancel any pending ops */\r |
| 270 | dsk_dsw[drv] |= DSK_DSW_DISK_BUSY; /* and mark drive as busy */\r |
| 271 | \r |
| 272 | nwords = M[iocc_addr++ & mem_mask]; /* get word count w/o upsetting SAR/SBR */\r |
| 273 | \r |
| 274 | if (nwords == 0) /* this is bad -- locks up disk controller ! */\r |
| 275 | break;\r |
| 276 | \r |
| 277 | if (! BETWEEN(nwords, 1, DSK_NUMWD)) { /* count bad */\r |
| 278 | SETBIT(uptr->flags, UNIT_OPERR); /* set data error DSW bit when op complete */\r |
| 279 | nwords = DSK_NUMWD; /* limit xfer to proper sector size */\r |
| 280 | }\r |
| 281 | \r |
| 282 | sec = modify & 0x07; /* get sector on cylinder */\r |
| 283 | newpos = (uptr->CYL*DSK_NUMSC*DSK_NUMSF + sec)*2*DSK_NUMWD;\r |
| 284 | \r |
| 285 | trace_io("* DSK%d wrote %d words from M[%04x-%04x] to %d.%d (%x, %x)", drv, nwords, iocc_addr & mem_mask, (iocc_addr + nwords - 1) & mem_mask, uptr->CYL, sec, uptr->CYL*8 + sec, newpos);\r |
| 286 | \r |
| 287 | if (raw_disk_debug)\r |
| 288 | printf("* DSK%d XIO @ %04x wrote %d words from M[%04x-%04x] to %d.%d (%x, %x)\n", drv, prev_IAR, nwords, iocc_addr & mem_mask, (iocc_addr + nwords - 1) & mem_mask, uptr->CYL, sec, uptr->CYL*8 + sec, newpos);\r |
| 289 | \r |
| 290 | #ifdef TRACE_DMS_IO\r |
| 291 | if (trace_dms)\r |
| 292 | tracesector(1, nwords, iocc_addr & mem_mask, uptr->CYL*8 + sec);\r |
| 293 | #endif\r |
| 294 | for (i = 0; i < nwords; i++)\r |
| 295 | buf[i] = M[iocc_addr++ & mem_mask];\r |
| 296 | \r |
| 297 | for (; i < DSK_NUMWD; i++) /* rest of sector gets zeroed */\r |
| 298 | buf[i] = 0;\r |
| 299 | \r |
| 300 | i = uptr->CYL*8 + sec;\r |
| 301 | if (buf[0] != i)\r |
| 302 | printf("*DSK writing bad sector#\n");\r |
| 303 | \r |
| 304 | if (MEM_MAPPED(uptr)) {\r |
| 305 | memcpy((char *) uptr->filebuf + newpos, buf, 2*DSK_NUMWD);\r |
| 306 | uptr->hwmark = newpos + 2*DSK_NUMWD;\r |
| 307 | }\r |
| 308 | else {\r |
| 309 | if (uptr->pos != newpos || dsk_lastio[drv] != IO_WRITE) {\r |
| 310 | fseek(uptr->fileref, newpos, SEEK_SET);\r |
| 311 | dsk_lastio[drv] = IO_WRITE;\r |
| 312 | }\r |
| 313 | \r |
| 314 | fxwrite(buf, 2, DSK_NUMWD, uptr->fileref);\r |
| 315 | uptr->pos = newpos + 2*DSK_NUMWD;\r |
| 316 | }\r |
| 317 | \r |
| 318 | uptr->FUNC = DSK_FUNC_WRITE;\r |
| 319 | sim_activate(uptr, dsk_rwait);\r |
| 320 | break;\r |
| 321 | \r |
| 322 | case XIO_CONTROL: /* step fwd/rev */\r |
| 323 | if (! IS_ONLINE(uptr)) {\r |
| 324 | diskfail(uptr, 0, 0, FALSE);\r |
| 325 | break;\r |
| 326 | }\r |
| 327 | \r |
| 328 | sim_cancel(uptr);\r |
| 329 | \r |
| 330 | rev = modify & 4;\r |
| 331 | nsteps = iocc_addr & 0x00FF;\r |
| 332 | if (nsteps == 0) /* 0 steps does not cause op complete interrupt */\r |
| 333 | break;\r |
| 334 | \r |
| 335 | newcyl = uptr->CYL + (rev ? (-nsteps) : nsteps);\r |
| 336 | if (newcyl < 0)\r |
| 337 | newcyl = 0;\r |
| 338 | else if (newcyl >= DSK_NUMCY)\r |
| 339 | newcyl = DSK_NUMCY-1;\r |
| 340 | \r |
| 341 | uptr->FUNC = DSK_FUNC_SEEK;\r |
| 342 | uptr->CYL = newcyl;\r |
| 343 | sim_activate(uptr, dsk_swait); /* schedule interrupt */\r |
| 344 | \r |
| 345 | dsk_dsw[drv] |= DSK_DSW_DISK_BUSY;\r |
| 346 | trace_io("* DSK%d at cyl %d", drv, newcyl);\r |
| 347 | break;\r |
| 348 | \r |
| 349 | case XIO_SENSE_DEV:\r |
| 350 | CLRBIT(dsk_dsw[drv], DSK_DSW_CARRIAGE_HOME|DSK_DSW_NOT_READY);\r |
| 351 | \r |
| 352 | if ((uptr->flags & UNIT_HARDERR) || (dsk_dsw[drv] & DSK_DSW_DISK_BUSY) || ! IS_ONLINE(uptr))\r |
| 353 | SETBIT(dsk_dsw[drv], DSK_DSW_NOT_READY);\r |
| 354 | else if (uptr->CYL <= 0) {\r |
| 355 | SETBIT(dsk_dsw[drv], DSK_DSW_CARRIAGE_HOME);\r |
| 356 | uptr->CYL = 0;\r |
| 357 | }\r |
| 358 | \r |
| 359 | dsk_sec[drv] = (int16) ((dsk_sec[drv] + 1) % 4); /* advance the "next sector" count every time */\r |
| 360 | ACC = dsk_dsw[drv] | dsk_sec[drv];\r |
| 361 | \r |
| 362 | if (modify & 0x01) { /* reset interrupts */\r |
| 363 | CLRBIT(dsk_dsw[drv], DSK_DSW_OP_COMPLETE|DSK_DSW_DATA_ERROR);\r |
| 364 | CLRBIT(ILSW[dsk_ilswlevel[drv]], dsk_ilswbit[drv]);\r |
| 365 | }\r |
| 366 | break;\r |
| 367 | \r |
| 368 | default:\r |
| 369 | sprintf(msg, "Invalid disk XIO function %x", func);\r |
| 370 | xio_error(msg);\r |
| 371 | }\r |
| 372 | }\r |
| 373 | \r |
| 374 | /* diskfail - schedule an operation complete that sets the error bit */\r |
| 375 | \r |
| 376 | static void diskfail (UNIT *uptr, int dswflag, int unitflag, t_bool do_interrupt)\r |
| 377 | {\r |
| 378 | int drv = uptr - dsk_unit;\r |
| 379 | \r |
| 380 | sim_cancel(uptr); /* cancel any pending ops */\r |
| 381 | SETBIT(dsk_dsw[drv], dswflag); /* set any specified DSW bits */\r |
| 382 | SETBIT(uptr->flags, unitflag); /* set any specified unit flag bits */\r |
| 383 | uptr->FUNC = DSK_FUNC_FAILED; /* tell svc routine why it failed */\r |
| 384 | \r |
| 385 | if (do_interrupt)\r |
| 386 | sim_activate(uptr, 1); /* schedule an immediate op complete interrupt */\r |
| 387 | }\r |
| 388 | \r |
| 389 | t_stat dsk_svc (UNIT *uptr)\r |
| 390 | {\r |
| 391 | int drv = uptr - dsk_unit, i, nwords, sec;\r |
| 392 | int16 buf[DSK_NUMWD];\r |
| 393 | uint32 newpos; /* changed from t_addr to uint32 in anticipation of simh 64-bit development */\r |
| 394 | int32 iocc_addr;\r |
| 395 | \r |
| 396 | if (uptr->FUNC == DSK_FUNC_IDLE) /* service function called with no activity? not good, but ignore */\r |
| 397 | return SCPE_OK;\r |
| 398 | \r |
| 399 | CLRBIT(dsk_dsw[drv], DSK_DSW_DISK_BUSY); /* activate operation complete interrupt */\r |
| 400 | SETBIT(dsk_dsw[drv], DSK_DSW_OP_COMPLETE);\r |
| 401 | \r |
| 402 | if (uptr->flags & (UNIT_OPERR|UNIT_HARDERR)) { /* word count error or data error */\r |
| 403 | SETBIT(dsk_dsw[drv], DSK_DSW_DATA_ERROR);\r |
| 404 | CLRBIT(uptr->flags, UNIT_OPERR); /* soft error is one time occurrence; don't clear hard error */\r |
| 405 | }\r |
| 406 | /* schedule interrupt */\r |
| 407 | SETBIT(ILSW[dsk_ilswlevel[drv]], dsk_ilswbit[drv]);\r |
| 408 | \r |
| 409 | switch (uptr->FUNC) { /* take care of business */\r |
| 410 | case DSK_FUNC_IDLE:\r |
| 411 | case DSK_FUNC_VERIFY:\r |
| 412 | case DSK_FUNC_WRITE:\r |
| 413 | case DSK_FUNC_SEEK:\r |
| 414 | case DSK_FUNC_FAILED:\r |
| 415 | break;\r |
| 416 | \r |
| 417 | case DSK_FUNC_READ: /* actually read the data into core */\r |
| 418 | iocc_addr = dsk_action[drv].io_address; /* recover saved parameters */\r |
| 419 | nwords = dsk_action[drv].io_nwords;\r |
| 420 | newpos = dsk_action[drv].io_filepos;\r |
| 421 | sec = dsk_action[drv].io_sector;\r |
| 422 | \r |
| 423 | if (MEM_MAPPED(uptr)) {\r |
| 424 | memcpy(buf, (char *) uptr->filebuf + newpos, 2*DSK_NUMWD);\r |
| 425 | }\r |
| 426 | else {\r |
| 427 | if (uptr->pos != newpos || dsk_lastio[drv] != IO_READ) {\r |
| 428 | fseek(uptr->fileref, newpos, SEEK_SET);\r |
| 429 | dsk_lastio[drv] = IO_READ;\r |
| 430 | uptr->pos = newpos;\r |
| 431 | }\r |
| 432 | fxread(buf, 2, DSK_NUMWD, uptr->fileref); /* read whole sector so we're in position for next read */\r |
| 433 | uptr->pos = newpos + 2*DSK_NUMWD;\r |
| 434 | }\r |
| 435 | \r |
| 436 | void_backtrace(iocc_addr, iocc_addr + nwords - 1); /* mark prev instruction as altered */\r |
| 437 | \r |
| 438 | trace_io("* DSK%d read %d words from %d.%d (%x, %x) to M[%04x-%04x]", drv, nwords, uptr->CYL, sec, uptr->CYL*8 + sec, newpos, iocc_addr & mem_mask,\r |
| 439 | (iocc_addr + nwords - 1) & mem_mask);\r |
| 440 | \r |
| 441 | /* this will help debug the monitor by letting me watch phase loading */\r |
| 442 | if (raw_disk_debug)\r |
| 443 | printf("* DSK%d XIO @ %04x read %d words from %d.%d (%x, %x) to M[%04x-%04x]\n", drv, prev_IAR, nwords, uptr->CYL, sec, uptr->CYL*8 + sec, newpos, iocc_addr & mem_mask,\r |
| 444 | (iocc_addr + nwords - 1) & mem_mask);\r |
| 445 | \r |
| 446 | i = uptr->CYL*8 + sec;\r |
| 447 | if (buf[0] != i)\r |
| 448 | printf("*DSK read bad sector #\n");\r |
| 449 | \r |
| 450 | for (i = 0; i < nwords; i++)\r |
| 451 | M[(iocc_addr+i) & mem_mask] = buf[i];\r |
| 452 | \r |
| 453 | #ifdef TRACE_DMS_IO\r |
| 454 | if (trace_dms)\r |
| 455 | tracesector(0, nwords, iocc_addr & mem_mask, uptr->CYL*8 + sec);\r |
| 456 | #endif\r |
| 457 | break;\r |
| 458 | \r |
| 459 | default:\r |
| 460 | fprintf(stderr, "Unexpected FUNC %x in dsk_svc(%d)\n", uptr->FUNC, drv);\r |
| 461 | break;\r |
| 462 | \r |
| 463 | }\r |
| 464 | \r |
| 465 | uptr->FUNC = DSK_FUNC_IDLE; /* we're done with this operation */\r |
| 466 | \r |
| 467 | return SCPE_OK;\r |
| 468 | }\r |
| 469 | \r |
| 470 | t_stat dsk_reset (DEVICE *dptr)\r |
| 471 | {\r |
| 472 | int drv;\r |
| 473 | UNIT *uptr;\r |
| 474 | \r |
| 475 | #ifdef TRACE_DMS_IO\r |
| 476 | /* add the WHERE command. It finds the phase that was loaded at given address and indicates */\r |
| 477 | /* the offset in the phase */\r |
| 478 | register_cmd("WHERE", &where_cmd, 0, "w{here} address find phase and offset of an address\n");\r |
| 479 | register_cmd("PHDEBUG", &phdebug_cmd, 0, "ph{debug} off|phlo phhi break on phase load\n");\r |
| 480 | register_cmd("FDUMP", &fdump_cmd, 0, NULL);\r |
| 481 | #endif\r |
| 482 | \r |
| 483 | for (drv = 0, uptr = dsk_dev.units; drv < DSK_NUMDR; drv++, uptr++) {\r |
| 484 | sim_cancel(uptr);\r |
| 485 | \r |
| 486 | CLRBIT(ILSW[2], dsk_ilswbit[drv]);\r |
| 487 | CLRBIT(uptr->flags, UNIT_OPERR|UNIT_HARDERR);\r |
| 488 | \r |
| 489 | uptr->CYL = 0;\r |
| 490 | uptr->FUNC = DSK_FUNC_IDLE;\r |
| 491 | dsk_dsw[drv] = (int16) ((uptr->flags & UNIT_ATT) ? DSK_DSW_CARRIAGE_HOME : 0);\r |
| 492 | }\r |
| 493 | \r |
| 494 | calc_ints();\r |
| 495 | \r |
| 496 | return SCPE_OK;\r |
| 497 | }\r |
| 498 | \r |
| 499 | static t_stat dsk_attach (UNIT *uptr, char *cptr)\r |
| 500 | {\r |
| 501 | int drv = uptr - dsk_unit;\r |
| 502 | t_stat rval;\r |
| 503 | \r |
| 504 | sim_cancel(uptr); /* cancel current IO */\r |
| 505 | dsk_lastio[drv] = IO_NONE;\r |
| 506 | \r |
| 507 | if (uptr->flags & UNIT_ATT) /* dismount current disk */\r |
| 508 | if ((rval = dsk_detach(uptr)) != SCPE_OK)\r |
| 509 | return rval;\r |
| 510 | \r |
| 511 | uptr->CYL = 0; /* reset the device */\r |
| 512 | uptr->FUNC = DSK_FUNC_IDLE;\r |
| 513 | dsk_dsw[drv] = DSK_DSW_CARRIAGE_HOME;\r |
| 514 | \r |
| 515 | CLRBIT(uptr->flags, UNIT_RO|UNIT_ROABLE|UNIT_BUFABLE|UNIT_BUF|UNIT_RONLY|UNIT_OPERR|UNIT_HARDERR);\r |
| 516 | CLRBIT(ILSW[2], dsk_ilswbit[drv]);\r |
| 517 | calc_ints();\r |
| 518 | \r |
| 519 | if (sim_switches & SWMASK('M')) /* if memory mode (e.g. for CGI), buffer the file */\r |
| 520 | SETBIT(uptr->flags, UNIT_BUFABLE|UNIT_MUSTBUF);\r |
| 521 | \r |
| 522 | if (sim_switches & SWMASK('R')) /* read lock mode */\r |
| 523 | SETBIT(uptr->flags, UNIT_RO|UNIT_ROABLE|UNIT_RONLY);\r |
| 524 | \r |
| 525 | if (cgi && (sim_switches & SWMASK('M')) && ! cgiwritable) { /* if cgi and memory mode, but writable option not specified */\r |
| 526 | sim_switches |= SWMASK('R'); /* have attach_unit open file in readonly mode */\r |
| 527 | SETBIT(uptr->flags, UNIT_ROABLE); /* but don't set the UNIT_RONLY flag so DMS can write to the buffered image */\r |
| 528 | }\r |
| 529 | \r |
| 530 | if ((rval = attach_unit(uptr, quotefix(cptr))) != SCPE_OK) { /* mount new disk */\r |
| 531 | SETBIT(dsk_dsw[drv], DSK_DSW_NOT_READY);\r |
| 532 | return rval;\r |
| 533 | }\r |
| 534 | \r |
| 535 | if (drv == 0) {\r |
| 536 | disk_ready(TRUE);\r |
| 537 | disk_unlocked(FALSE);\r |
| 538 | }\r |
| 539 | \r |
| 540 | enable_dms_tracing(sim_switches & SWMASK('D'));\r |
| 541 | raw_disk_debug = sim_switches & SWMASK('G');\r |
| 542 | \r |
| 543 | return SCPE_OK;\r |
| 544 | }\r |
| 545 | \r |
| 546 | static t_stat dsk_detach (UNIT *uptr)\r |
| 547 | {\r |
| 548 | t_stat rval;\r |
| 549 | int drv = uptr - dsk_unit;\r |
| 550 | \r |
| 551 | sim_cancel(uptr);\r |
| 552 | \r |
| 553 | if ((rval = detach_unit(uptr)) != SCPE_OK)\r |
| 554 | return rval;\r |
| 555 | \r |
| 556 | CLRBIT(ILSW[2], dsk_ilswbit[drv]);\r |
| 557 | CLRBIT(uptr->flags, UNIT_OPERR|UNIT_HARDERR);\r |
| 558 | calc_ints();\r |
| 559 | \r |
| 560 | uptr->CYL = 0;\r |
| 561 | uptr->FUNC = DSK_FUNC_IDLE;\r |
| 562 | dsk_dsw[drv] = DSK_DSW_NOT_READY;\r |
| 563 | \r |
| 564 | if (drv == 0) {\r |
| 565 | disk_unlocked(TRUE);\r |
| 566 | disk_ready(FALSE);\r |
| 567 | }\r |
| 568 | \r |
| 569 | return SCPE_OK;\r |
| 570 | }\r |
| 571 | \r |
| 572 | /* boot routine - if they type BOOT DSK, load the standard boot card. */\r |
| 573 | \r |
| 574 | static t_stat dsk_boot (int unitno, DEVICE *dptr)\r |
| 575 | {\r |
| 576 | t_stat rval;\r |
| 577 | \r |
| 578 | if ((rval = reset_all(0)) != SCPE_OK)\r |
| 579 | return rval;\r |
| 580 | \r |
| 581 | return load_cr_boot(unitno, sim_switches);\r |
| 582 | }\r |
| 583 | \r |
| 584 | #ifdef TRACE_DMS_IO\r |
| 585 | \r |
| 586 | static struct {\r |
| 587 | int phid;\r |
| 588 | char *name;\r |
| 589 | } phase[] = {\r |
| 590 | # include "dmsr2v12phases.h"\r |
| 591 | 0xFFFF, ""\r |
| 592 | };\r |
| 593 | \r |
| 594 | #pragma pack(2)\r |
| 595 | #define MAXSLET ((3*320)/4)\r |
| 596 | struct tag_slet {\r |
| 597 | int16 phid;\r |
| 598 | int16 addr;\r |
| 599 | int16 nwords;\r |
| 600 | int16 sector;\r |
| 601 | } slet[MAXSLET] = {\r |
| 602 | # include "dmsr2v12slet.h" /* without RPG, use this info until overwritten by actual data from disk */\r |
| 603 | };\r |
| 604 | \r |
| 605 | #pragma pack()\r |
| 606 | \r |
| 607 | #define MAXMSEG 100\r |
| 608 | struct tag_mseg {\r |
| 609 | char *name;\r |
| 610 | int addr, offset, len, phid;\r |
| 611 | } mseg[MAXMSEG];\r |
| 612 | int nseg = 0;\r |
| 613 | \r |
| 614 | static void enable_dms_tracing (int newsetting)\r |
| 615 | {\r |
| 616 | nseg = 0; /* clear the segment map */\r |
| 617 | \r |
| 618 | if ((newsetting && trace_dms) || ! (newsetting || trace_dms))\r |
| 619 | return;\r |
| 620 | \r |
| 621 | trace_dms = newsetting;\r |
| 622 | if (! sim_quiet)\r |
| 623 | printf("DMS disk tracing is now %sabled\n", trace_dms ? "en" : "dis");\r |
| 624 | }\r |
| 625 | \r |
| 626 | char * saywhere (int addr)\r |
| 627 | {\r |
| 628 | int i;\r |
| 629 | static char buf[150];\r |
| 630 | \r |
| 631 | for (i = 0; i < nseg; i++) {\r |
| 632 | if (addr >= mseg[i].addr && addr < (mseg[i].addr+mseg[i].len)) {\r |
| 633 | sprintf(buf, "/%04x = /%04x + /%x in ", addr, mseg[i].addr - mseg[i].offset, addr-mseg[i].addr + mseg[i].offset);\r |
| 634 | if (mseg[i].phid > 0) \r |
| 635 | sprintf(buf+strlen(buf), "phase %02x (%s)", mseg[i].phid, mseg[i].name);\r |
| 636 | else\r |
| 637 | sprintf(buf+strlen(buf), "%s", mseg[i].name);\r |
| 638 | \r |
| 639 | return buf;\r |
| 640 | }\r |
| 641 | }\r |
| 642 | return NULL;\r |
| 643 | }\r |
| 644 | \r |
| 645 | static int phdebug_lo = -1, phdebug_hi = -1;\r |
| 646 | \r |
| 647 | static t_stat phdebug_cmd (int flag, char *ptr)\r |
| 648 | {\r |
| 649 | int val1, val2;\r |
| 650 | \r |
| 651 | if (strcmpi(ptr, "off") == 0)\r |
| 652 | phdebug_lo = phdebug_hi = -1;\r |
| 653 | else {\r |
| 654 | switch(sscanf(ptr, "%x%x", &val1, &val2)) {\r |
| 655 | case 1:\r |
| 656 | phdebug_lo = phdebug_hi = val1;\r |
| 657 | enable_dms_tracing(TRUE);\r |
| 658 | break;\r |
| 659 | \r |
| 660 | case 2:\r |
| 661 | phdebug_lo = val1;\r |
| 662 | phdebug_hi = val2;\r |
| 663 | enable_dms_tracing(TRUE);\r |
| 664 | break;\r |
| 665 | \r |
| 666 | default:\r |
| 667 | printf("Usage: phdebug off | phdebug phfrom [phto]\n");\r |
| 668 | break;\r |
| 669 | }\r |
| 670 | }\r |
| 671 | return SCPE_OK;\r |
| 672 | }\r |
| 673 | \r |
| 674 | static t_stat where_cmd (int flag, char *ptr)\r |
| 675 | {\r |
| 676 | int addr;\r |
| 677 | char *where;\r |
| 678 | \r |
| 679 | if (! trace_dms) {\r |
| 680 | printf("Tracing is disabled. To enable, attach disk with -d switch\n");\r |
| 681 | return SCPE_OK;\r |
| 682 | }\r |
| 683 | \r |
| 684 | if (sscanf(ptr, "%x", &addr) != 1)\r |
| 685 | return SCPE_ARG;\r |
| 686 | \r |
| 687 | if ((where = saywhere(addr)) == NULL)\r |
| 688 | printf("/%04x not found\n", addr);\r |
| 689 | else\r |
| 690 | printf("%s\n", where);\r |
| 691 | \r |
| 692 | return SCPE_OK;\r |
| 693 | }\r |
| 694 | \r |
| 695 | /* savesector - save info on a sector just read. THIS IS NOT YET TESTED */\r |
| 696 | \r |
| 697 | static void addseg (int i)\r |
| 698 | {\r |
| 699 | if (! trace_dms)\r |
| 700 | return;\r |
| 701 | \r |
| 702 | if (nseg >= MAXMSEG) {\r |
| 703 | printf("(Memory map full, disabling tracing)\n");\r |
| 704 | trace_dms = 0;\r |
| 705 | nseg = -1;\r |
| 706 | return;\r |
| 707 | }\r |
| 708 | memcpy(mseg+i+1, mseg+i, (nseg-i)*sizeof(mseg[0]));\r |
| 709 | nseg++;\r |
| 710 | }\r |
| 711 | \r |
| 712 | static void delseg (int i)\r |
| 713 | {\r |
| 714 | if (! trace_dms)\r |
| 715 | return;\r |
| 716 | \r |
| 717 | if (nseg > 0) {\r |
| 718 | nseg--;\r |
| 719 | memcpy(mseg+i, mseg+i+1, (nseg-i)*sizeof(mseg[0]));\r |
| 720 | }\r |
| 721 | }\r |
| 722 | \r |
| 723 | static void savesector (int addr, int offset, int len, int phid, char *name)\r |
| 724 | {\r |
| 725 | int i;\r |
| 726 | \r |
| 727 | if (! trace_dms)\r |
| 728 | return;\r |
| 729 | \r |
| 730 | addr++; /* first word is sector address, so account for that */\r |
| 731 | len--;\r |
| 732 | \r |
| 733 | for (i = 0; i < nseg; i++) {\r |
| 734 | if (addr >= (mseg[i].addr+mseg[i].len)) /* entirely after this entry */\r |
| 735 | continue;\r |
| 736 | \r |
| 737 | if (mseg[i].addr < addr) { /* old one starts before this. split it */\r |
| 738 | addseg(i);\r |
| 739 | mseg[i].len = addr-mseg[i].addr;\r |
| 740 | i++;\r |
| 741 | mseg[i].addr = addr;\r |
| 742 | mseg[i].len -= mseg[i-1].len;\r |
| 743 | }\r |
| 744 | \r |
| 745 | break;\r |
| 746 | }\r |
| 747 | \r |
| 748 | addseg(i); /* add new segment. Old one ends up after this */\r |
| 749 | \r |
| 750 | if (i >= MAXMSEG)\r |
| 751 | return;\r |
| 752 | \r |
| 753 | mseg[i].addr = addr;\r |
| 754 | mseg[i].offset = offset;\r |
| 755 | mseg[i].phid = phid;\r |
| 756 | mseg[i].len = len;\r |
| 757 | mseg[i].name = name;\r |
| 758 | \r |
| 759 | i++; /* delete any segments completely covered */\r |
| 760 | \r |
| 761 | while (i < nseg && (mseg[i].addr+mseg[i].len) <= (addr+len))\r |
| 762 | delseg(i);\r |
| 763 | \r |
| 764 | if (i < nseg && mseg[i].addr < (addr+len)) { /* old one extends past this. Retain the end */\r |
| 765 | mseg[i].len = (mseg[i].addr+mseg[i].len) - (addr+len);\r |
| 766 | mseg[i].addr = addr+len;\r |
| 767 | }\r |
| 768 | }\r |
| 769 | \r |
| 770 | static void tracesector (int iswrite, int nwords, int addr, int sector)\r |
| 771 | {\r |
| 772 | int i, phid = 0, offset = 0;\r |
| 773 | char *name = NULL;\r |
| 774 | \r |
| 775 | if (nwords < 3 || ! trace_dms)\r |
| 776 | return;\r |
| 777 | \r |
| 778 | switch (sector) { /* explicitly known sector name */\r |
| 779 | case 0: name = "ID/COLD START"; break;\r |
| 780 | case 1: name = "DCOM"; break;\r |
| 781 | case 2: name = "RESIDENT IMAGE"; break;\r |
| 782 | case 3:\r |
| 783 | case 4:\r |
| 784 | case 5: name = "SLET"; /* save just-read or written SLET info */\r |
| 785 | memmove(&slet[(320/4)*(sector-3)], &M[addr+1], nwords*2);\r |
| 786 | break;\r |
| 787 | case 6: name = "RELOAD TABLE"; break;\r |
| 788 | case 7: name = "PAGE HEADER"; break;\r |
| 789 | }\r |
| 790 | \r |
| 791 | printf("* %04x: %3d /%04x %c %3d.%d ",\r |
| 792 | prev_IAR, nwords, addr, iswrite ? 'W' : 'R', sector/8, sector%8);\r |
| 793 | \r |
| 794 | if (name == NULL) { /* look up sector in SLET */\r |
| 795 | for (i = 0; i < MAXSLET; i++) {\r |
| 796 | if (slet[i].phid == 0) /* not found */\r |
| 797 | goto done;\r |
| 798 | else if (slet[i].sector > sector) {\r |
| 799 | if (--i >= 0) {\r |
| 800 | if (sector >= slet[i].sector && sector <= (slet[i].sector + slet[i].nwords/320)) {\r |
| 801 | phid = slet[i].phid;\r |
| 802 | offset = (sector-slet[i].sector)*320;\r |
| 803 | break;\r |
| 804 | }\r |
| 805 | }\r |
| 806 | goto done;\r |
| 807 | }\r |
| 808 | if (slet[i].sector == sector) {\r |
| 809 | phid = slet[i].phid; /* we found the starting sector */\r |
| 810 | break;\r |
| 811 | }\r |
| 812 | }\r |
| 813 | \r |
| 814 | if (i >= MAXSLET) /* was not found */\r |
| 815 | goto done;\r |
| 816 | \r |
| 817 | name = "?";\r |
| 818 | for (i = sizeof(phase)/sizeof(phase[0]); --i >= 0; ) {\r |
| 819 | if (phase[i].phid == phid) { /* look up name */\r |
| 820 | name = phase[i].name;\r |
| 821 | break;\r |
| 822 | }\r |
| 823 | }\r |
| 824 | printf("%02x %s", phid, name);\r |
| 825 | }\r |
| 826 | else\r |
| 827 | printf("%s", name);\r |
| 828 | \r |
| 829 | done:\r |
| 830 | putchar('\n');\r |
| 831 | \r |
| 832 | if (phid >= phdebug_lo && phid <= phdebug_hi && offset == 0)\r |
| 833 | break_simulation(STOP_PHASE_BREAK); /* break on read of first sector of indicated phases */\r |
| 834 | \r |
| 835 | if (name != NULL && *name != '?' && ! iswrite)\r |
| 836 | savesector(addr, offset, nwords, phid, name);\r |
| 837 | }\r |
| 838 | \r |
| 839 | static t_stat fdump_cmd (int flags, char *cptr)\r |
| 840 | {\r |
| 841 | int addr = 0x7a24; /* address of next statement */\r |
| 842 | int sofst = 0x7a26, symaddr;\r |
| 843 | int cword, nwords, stype, has_stnum, strel = 1, laststno = 0;\r |
| 844 | \r |
| 845 | addr = M[addr & mem_mask] & mem_mask; /* get address of first statement */\r |
| 846 | sofst = M[sofst & mem_mask] & mem_mask ; /* get address of symbol table */\r |
| 847 | \r |
| 848 | for (;;) {\r |
| 849 | cword = M[addr];\r |
| 850 | nwords = (cword >> 2) & 0x01FF;\r |
| 851 | stype = (cword >> 1) & 0x7C00;\r |
| 852 | has_stnum = (cword & 1);\r |
| 853 | \r |
| 854 | if (has_stnum) {\r |
| 855 | laststno++;\r |
| 856 | strel = 0;\r |
| 857 | }\r |
| 858 | \r |
| 859 | printf("/%04x [%4d +%3d] %3d - %04x", addr, laststno, strel, nwords, stype);\r |
| 860 | \r |
| 861 | if (has_stnum) {\r |
| 862 | addr++;\r |
| 863 | nwords--;\r |
| 864 | symaddr = sofst - (M[addr] & 0x7FF)*3 + 3;\r |
| 865 | printf(" [%04x %04x %04x]", M[symaddr], M[symaddr+1], M[symaddr+2]);\r |
| 866 | }\r |
| 867 | \r |
| 868 | if (stype == 0x5000) { /* error record */\r |
| 869 | printf(" (err %d)", M[addr+1]);\r |
| 870 | }\r |
| 871 | \r |
| 872 | if (stype == 0x0800)\r |
| 873 | break;\r |
| 874 | \r |
| 875 | addr += nwords;\r |
| 876 | putchar('\n');\r |
| 877 | \r |
| 878 | if (nwords == 0) {\r |
| 879 | printf("0 words?\n");\r |
| 880 | break;\r |
| 881 | }\r |
| 882 | strel++;\r |
| 883 | }\r |
| 884 | \r |
| 885 | printf("\nEnd found at /%04x, EOFS = /%04x\n", addr, M[0x7a25 & mem_mask]);\r |
| 886 | return SCPE_OK;\r |
| 887 | }\r |
| 888 | \r |
| 889 | #endif /* TRACE_DMS_IO */\r |