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