1 /*
2 * Z80SIM - a Z80-CPU simulator
3 *
4 * Common I/O devices used by various simulated machines
5 *
6 * Copyright (C) 2018-2019 by Udo Munk
7 *
8 * Emulation of the MITS Altair S100 floppy disk controller
9 *
10 * History:
11 * 10-AUG-2018 first version, runs CP/M 1.4 & 2.2 & disk BASIC
12 * 02-DEC-2019 use disk names different from Tarbell controller
13 */
14
15 #include <pthread.h>
16 #include <unistd.h>
17 #include <stdlib.h>
18 #include <stdio.h>
19 #include <string.h>
20 #include <sys/stat.h>
21 #include <fcntl.h>
22 #include "sim.h"
23 #include "simglb.h"
24 /* #define LOG_LOCAL_LEVEL LOG_DEBUG */
25 #include "log.h"
26
27 /*
28 * Time in milliseconds for the disk mechanics. Can be tuned
29 * to speed up drive, minimum possible value is 1.
30 */
31 #define TIMESTEP 10 /* time to step one track */
32 #define TIMELOAD 40 /* time until head settled after load/step */
33 #define TIMESEC 5 /* time for sector under head at 360 RPM */
34
35 /* internal state of the fdc */
36 #define FDC_DISABLED 0 /* FDC and disk are disabled */
37 #define FDC_ENABLED 1 /* FDC and disk are enabled */
38
39 /* fdc status bits */
40 #define ENWD 1 /* enter new write data */
41 #define MOVEHD 2 /* indicates when head movement allowed */
42 #define STATHD 4 /* indicates when head properly loaded */
43 #define INTE 32 /* CPU INTE */
44 #define TRACK0 64 /* track 0 detected */
45 #define NRDA 128 /* new read data available */
46
47 /* disk format */
48 #define SEC_SZ 137
49 #define SPT 32
50 #define TRK 77
51
52 static const char *TAG = "88DCDD";
53
54 static int track[16]; /* current track of the disks */
55 static int sec; /* current sector position */
56 static int rwsec; /* sector read/written */
57 static int disk; /* current disk # */
58 static BYTE status = 0xff; /* controller status */
59 static pthread_mutex_t mustatus = PTHREAD_MUTEX_INITIALIZER;
60 static int headloaded; /* head loaded flag */
61 static int writing; /* write circuit enabled */
62 static int state; /* fdc state */
63 static char fn[MAX_LFN]; /* path/filename for disk image */
64 static int fd; /* fd for disk file i/o */
65 static int dcnt; /* data counter read/write */
66 static BYTE buf[SEC_SZ]; /* buffer for one sector */
67
68 static int cnt_sec; /* counter for sector position */
69 static int cnt_head; /* counter for loading head */
70 static int cnt_step; /* counter for stepping track */
71
72 static pthread_t thread; /* thread for timing */
73
74 /* these are our disk drives */
75 static char *disks[16] = {
76 "mits_a.dsk",
77 "mits_b.dsk",
78 "mits_c.dsk",
79 "mits_d.dsk",
80 "mits_e.dsk",
81 "mits_f.dsk",
82 "mits_g.dsk",
83 "mits_h.dsk",
84 "mits_i.dsk",
85 "mits_j.dsk",
86 "mits_k.dsk",
87 "mits_l.dsk",
88 "mits_m.dsk",
89 "mits_n.dsk",
90 "mits_o.dsk",
91 "mits_p.dsk"
92 };
93
94 /*
95 * find and set path for disk images
96 */
dsk_path(void)97 static void dsk_path(void)
98 {
99 struct stat sbuf;
100
101 /* if option -d is used disks are there */
102 if (diskdir != NULL) {
103 strcpy(fn, diskd);
104 } else {
105 /* if not first try ./disks */
106 if ((stat("./disks", &sbuf) == 0) && S_ISDIR(sbuf.st_mode)) {
107 strcpy(fn, "./disks");
108 /* nope, then DISKSDIR as set in Makefile */
109 } else {
110 strcpy(fn, DISKSDIR);
111 }
112 }
113 }
114
115 /*
116 * check disk image
117 */
dsk_check(void)118 static int dsk_check(void)
119 {
120 struct stat s;
121
122 /* try to open disk image */
123 dsk_path();
124 strcat(fn, "/");
125 strcat(fn, disks[disk]);
126 if ((fd = open(fn, O_RDONLY)) == -1)
127 return(0);
128
129 /* check for correct image size */
130 fstat(fd, &s);
131 close(fd);
132 if (s.st_size != 337568)
133 return(0);
134 else
135 return(1);
136 }
137
138 /*
139 * disable disk system
140 */
dsk_disable(void)141 static void dsk_disable(void)
142 {
143 state = FDC_DISABLED;
144 pthread_mutex_lock(&mustatus);
145 status = 0xff;
146 pthread_mutex_unlock(&mustatus);
147 headloaded = 0;
148 writing = 0;
149 cnt_head = 0;
150 cnt_step = 0;
151 if (thread != 0) {
152 pthread_cancel(thread);
153 pthread_join(thread, NULL);
154 thread = 0;
155 }
156 LOGD(TAG, "disabled");
157 }
158
159 /*
160 * thread for timing
161 */
timing(void * arg)162 static void *timing(void *arg)
163 {
164 arg = arg; /* to avoid compiler warning */
165
166 /* 1 msec per loop iteration */
167 while (1) {
168 /* advance sector position, 5ms at 360 RPM */
169 if (++cnt_sec > TIMESEC) {
170 cnt_sec = 0;
171 if (++sec >= SPT) {
172 sec = 0;
173 }
174 }
175
176 /* count down head load timer */
177 if (cnt_head > 0) {
178 if (--cnt_head == 0) {
179 pthread_mutex_lock(&mustatus);
180 status &= ~STATHD;
181 pthread_mutex_unlock(&mustatus);
182 LOGD(TAG, "head loaded");
183 }
184 }
185
186 /* count down stepping timer */
187 if (cnt_step > 0) {
188 if (--cnt_step == 0) {
189 pthread_mutex_lock(&mustatus);
190 status &= ~MOVEHD;
191 pthread_mutex_unlock(&mustatus);
192 }
193 }
194
195 /* sleep for 1 millisecond */
196 SLEEP_MS(1);
197 }
198
199 pthread_exit(NULL);
200 }
201
202 /*
203 * enables/disables controller and selects disk drives
204 */
altair_dsk_select_out(BYTE data)205 void altair_dsk_select_out(BYTE data)
206 {
207 /* disable? */
208 if (data & 0x80) {
209 dsk_disable();
210 /* no, enable */
211 } else {
212 /* get disk no. */
213 disk = data & 0x0f;
214 /* check disk in drive */
215 if (dsk_check() == 0) {
216 /* no (valid) disk in drive, disable */
217 dsk_disable();
218 return;
219 }
220 /* enable */
221 state = FDC_ENABLED;
222 pthread_mutex_lock(&mustatus);
223 status = 0b10100101;
224 pthread_mutex_unlock(&mustatus);
225 writing = 0;
226 headloaded = 0;
227 cnt_head = 0;
228 cnt_step = 0;
229 if (thread == 0) {
230 if (pthread_create(&thread, NULL, timing,
231 (void *) NULL)) {
232 LOGE(TAG, "can't create timing thread");
233 exit(1);
234 }
235 }
236 LOGD(TAG, "enabled, disk = %d", disk);
237 }
238 }
239
240 /*
241 * Status when drive and controller enabled
242 * True condition = 0, False = 1
243 *
244 * D0 enter new write data
245 * D1 indicates when head movement allowed
246 * D2 indicates when head is properly loaded
247 * D3 not used, = 0 (software uses this to figure if enabled!)
248 * D4 not used, = 0
249 * D5 CPU INTE
250 * D6 track 0 detected
251 * D7 new read data available
252 */
altair_dsk_status_in(void)253 BYTE altair_dsk_status_in(void)
254 {
255 if (state == FDC_ENABLED) {
256 /* set CPU INTE */
257 if (IFF & 1) {
258 pthread_mutex_lock(&mustatus);
259 status &= ~INTE;
260 pthread_mutex_unlock(&mustatus);
261 } else {
262 pthread_mutex_lock(&mustatus);
263 status |= INTE;
264 pthread_mutex_unlock(&mustatus);
265 }
266
267 /* set track 0 */
268 if (track[disk] == 0) {
269 pthread_mutex_lock(&mustatus);
270 status &= ~TRACK0;
271 pthread_mutex_unlock(&mustatus);
272 } else {
273 pthread_mutex_lock(&mustatus);
274 status |= TRACK0;
275 pthread_mutex_unlock(&mustatus);
276 }
277 }
278
279 return(status);
280 }
281
282 /*
283 * Controls disk operations when disk drive and controller enabled:
284 *
285 * D0 Step in - steps disk head in to higher numbered track
286 * D1 Step out - steps disk head out to lower numbered track
287 * D2 Head load - loads head, enables sector position status
288 * D3 Head unload - removes head
289 * D4 Interrupt enable - n.i.
290 * D5 Interrupt disable - n.i.
291 * D6 Head current switch - don't care
292 * F7 Write enable - start writing sector
293 */
altair_dsk_control_out(BYTE data)294 void altair_dsk_control_out(BYTE data)
295 {
296 if (state == FDC_ENABLED) {
297 /* step in */
298 if (data & 1) {
299 LOGD(TAG, "step in from track %d", track[disk]);
300 if (track[disk] < (TRK - 1)) {
301 track[disk]++;
302 pthread_mutex_lock(&mustatus);
303 status |= MOVEHD;
304 pthread_mutex_unlock(&mustatus);
305 cnt_step = TIMESTEP;
306 /* head needs to settle again */
307 if (headloaded) {
308 pthread_mutex_lock(&mustatus);
309 status |= STATHD;
310 pthread_mutex_unlock(&mustatus);
311 cnt_head = TIMELOAD;
312 }
313 }
314 }
315
316 /* step out */
317 if (data & 2) {
318 LOGD(TAG, "step out from track %d", track[disk]);
319 if (track[disk] > 0) {
320 track[disk]--;
321 pthread_mutex_lock(&mustatus);
322 status |= MOVEHD;
323 pthread_mutex_unlock(&mustatus);
324 cnt_step = TIMESTEP;
325 /* head needs to settle again */
326 if (headloaded) {
327 pthread_mutex_lock(&mustatus);
328 status |= STATHD;
329 pthread_mutex_unlock(&mustatus);
330 cnt_head = TIMELOAD;
331 }
332 }
333 }
334
335 /* load head */
336 if (data & 4) {
337 headloaded = 1;
338 cnt_head = TIMELOAD;
339 pthread_mutex_lock(&mustatus);
340 status |= MOVEHD;
341 pthread_mutex_unlock(&mustatus);
342 cnt_step = TIMELOAD;
343 LOGD(TAG, "load head");
344 }
345
346 /* unload head */
347 if (data & 8) {
348 headloaded = 0;
349 cnt_head = 0;
350 pthread_mutex_lock(&mustatus);
351 status |= STATHD;
352 pthread_mutex_unlock(&mustatus);
353 LOGD(TAG, "unload head");
354 }
355
356 /* write enable */
357 if ((data & 128) && (writing == 0)) {
358 writing = 1;
359 dcnt = 0;
360 pthread_mutex_lock(&mustatus);
361 status &= ~ENWD;
362 pthread_mutex_unlock(&mustatus);
363 LOGD(TAG, "write enabled");
364 }
365 }
366 }
367
368 /*
369 * FDC sector position
370 */
altair_dsk_sec_in(void)371 BYTE altair_dsk_sec_in(void)
372 {
373 BYTE sectrue;
374
375 if ((state != FDC_ENABLED) || (status & STATHD)) {
376 pthread_mutex_lock(&mustatus);
377 status |= NRDA;
378 status |= ENWD;
379 pthread_mutex_unlock(&mustatus);
380 return(0xff);
381 } else {
382 if (sec != rwsec) {
383 rwsec = sec;
384 sectrue = 0; /* start of new sector */
385 pthread_mutex_lock(&mustatus);
386 status &= ~NRDA; /* new read data available */
387 status |= ENWD; /* not ready for writing */
388 pthread_mutex_unlock(&mustatus);
389 dcnt = 0;
390 } else {
391 sectrue = 1;
392 }
393 }
394
395 return(sectrue + (rwsec << 1));
396 }
397
398 /*
399 * FDC data output port
400 */
altair_dsk_data_out(BYTE data)401 void altair_dsk_data_out(BYTE data)
402 {
403 long pos;
404
405 /* don't write past buffer */
406 if (dcnt >= SEC_SZ)
407 return;
408
409 /* put data into buffer and increment counter */
410 buf[dcnt++] = data;
411
412 /* last byte written? */
413 if (dcnt == SEC_SZ) {
414 writing = 0;
415 /* check disk */
416 if (dsk_check() == 0) {
417 dsk_disable();
418 return;
419 }
420 /* write sector */
421 fd = open(fn, O_RDWR);
422 pos = (track[disk] * SPT + rwsec) * SEC_SZ;
423 lseek(fd, pos, SEEK_SET);
424 write(fd, &buf[0], SEC_SZ);
425 close(fd);
426 LOGD(TAG, "write sector %d track %d", rwsec, track[disk]);
427 }
428 }
429
430 /*
431 * FDC data input port
432 */
altair_dsk_data_in(void)433 BYTE altair_dsk_data_in(void)
434 {
435 BYTE data;
436 long pos;
437
438 /* first byte? */
439 if (dcnt == 0) {
440 /* check disk */
441 if (dsk_check() == 0) {
442 dsk_disable();
443 memset(&buf[0], 0xff, SEC_SZ);
444 } else {
445 /* read sector */
446 fd = open(fn, O_RDONLY);
447 pos = (track[disk] * SPT + rwsec) * SEC_SZ;
448 lseek(fd, pos, SEEK_SET);
449 read(fd, &buf[0], SEC_SZ);
450 close(fd);
451 LOGD(TAG, "read sector %d track %d", rwsec, track[disk]);
452 }
453 }
454
455 /* no more data? */
456 if (dcnt == SEC_SZ)
457 return(0xff);
458
459 /* return byte from buffer and increment counter */
460 data = buf[dcnt++];
461 if (dcnt == SEC_SZ) {
462 pthread_mutex_lock(&mustatus);
463 status |= NRDA; /* no more data to read */
464 pthread_mutex_unlock(&mustatus);
465 }
466 return(data);
467 }
468
469 /*
470 * Reset FDC
471 */
altair_dsk_reset(void)472 void altair_dsk_reset(void)
473 {
474 state = FDC_DISABLED;
475 pthread_mutex_lock(&mustatus);
476 status = 0xff;
477 pthread_mutex_unlock(&mustatus);
478 headloaded = writing = dcnt = 0;
479 cnt_head = cnt_step = 0;
480 }
481