OXYGENHUB
◆ main openmsx / src / fdc / WD2793.cc
31.9 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "WD2793.hh"
2
3
#include "DiskDrive.hh"
4
5
#include "Clock.hh"
6
#include "MSXCliComm.hh"
7
#include "MSXException.hh"
8
#include "serialize.hh"
9
10
#include "narrow.hh"
11
#include "unreachable.hh"
12
13
#include <array>
14
15
namespace openmsx {
16
17
// Status register
18
static constexpr int BUSY             = 0x01;
19
static constexpr int INDEX            = 0x02;
20
static constexpr int S_DRQ            = 0x02;
21
static constexpr int TRACK00          = 0x04;
22
static constexpr int LOST_DATA        = 0x04;
23
static constexpr int CRC_ERROR        = 0x08;
24
static constexpr int SEEK_ERROR       = 0x10;
25
static constexpr int RECORD_NOT_FOUND = 0x10;
26
static constexpr int HEAD_LOADED      = 0x20;
27
static constexpr int RECORD_TYPE      = 0x20;
28
static constexpr int WRITE_PROTECTED  = 0x40;
29
static constexpr int NOT_READY        = 0x80;
30
31
// Command register
32
static constexpr int STEP_SPEED = 0x03;
33
static constexpr int V_FLAG     = 0x04;
34
static constexpr int E_FLAG     = 0x04;
35
static constexpr int H_FLAG     = 0x08;
36
static constexpr int T_FLAG     = 0x10;
37
static constexpr int M_FLAG     = 0x10;
38
static constexpr int A0_FLAG    = 0x01;
39
static constexpr int N2R_IRQ    = 0x01;
40
static constexpr int R2N_IRQ    = 0x02;
41
static constexpr int IDX_IRQ    = 0x04;
42
static constexpr int IMM_IRQ    = 0x08;
43
44
// HLD/HLT timing constants
45
static constexpr auto IDLE = EmuDuration::sec(3);
46
47
/** This class has emulation for WD1770, WD1793, WD2793. Though at the moment
48
 * the only emulated difference between WD1770 and WD{12}793 is that WD1770
49
 * has no ready input signal. (E.g. we don't emulate the WD1770 motor out
50
 * signal yet).
51
 */
52
WD2793::WD2793(Scheduler& scheduler_, DiskDrive& drive_, MSXCliComm& cliComm_,
53
               EmuTime time, bool isWD1770_)
54
	: Schedulable(scheduler_)
55
	, drive(drive_)
56
	, cliComm(cliComm_)
57
	, isWD1770(isWD1770_)
58
{
59
	setDrqRate(RawTrack::STANDARD_SIZE);
60
	reset(time);
61
}
62
63
void WD2793::reset(EmuTime time)
64
{
65
	removeSyncPoint();
66
	fsmState = FSM::NONE;
67
68
	statusReg = 0;
69
	trackReg = 0;
70
	dataReg = 0;
71
	directionIn = true;
72
73
	drqTime.reset(EmuTime::infinity()); // DRQ = false
74
	irqTime = EmuTime::infinity();      // INTRQ = false;
75
	immediateIRQ = false;
76
77
	// Execute Restore command
78
	sectorReg = 0x01;
79
	setCommandReg(0x03, time);
80
}
81
82
bool WD2793::getDTRQ(EmuTime time) const
83
{
84
	return peekDTRQ(time);
85
}
86
87
bool WD2793::peekDTRQ(EmuTime time) const
88
{
89
	return time >= drqTime.getTime();
90
}
91
92
void WD2793::setDrqRate(unsigned trackLength)
93
{
94
	drqTime.setFreq(trackLength * DiskDrive::ROTATIONS_PER_SECOND);
95
}
96
97
bool WD2793::getIRQ(EmuTime time) const
98
{
99
	return peekIRQ(time);
100
}
101
102
bool WD2793::peekIRQ(EmuTime time) const
103
{
104
	return immediateIRQ || (irqTime <= time);
105
}
106
107
bool WD2793::isReady() const
108
{
109
	// The WD1770 has no ready input signal (instead that pin is replaced
110
	// by a motor-on/off output pin).
111
	return drive.isDiskInserted() || isWD1770;
112
}
113
114
void WD2793::setCommandReg(uint8_t value, EmuTime time)
115
{
116
	if (((commandReg & 0xE0) == 0xA0) || // write sector
117
	    ((commandReg & 0xF0) == 0xF0)) { // write track
118
		// If a write sector/track command is cancelled, still flush
119
		// the partially written data to disk.
120
		try {
121
			drive.flushTrack();
122
		} catch (MSXException&) {
123
			// ignore
124
		}
125
	}
126
127
	removeSyncPoint();
128
129
	commandReg = value;
130
	irqTime = EmuTime::infinity(); // INTRQ = false;
131
	switch (commandReg & 0xF0) {
132
		case 0x00: // restore
133
		case 0x10: // seek
134
		case 0x20: // step
135
		case 0x30: // step (Update trackRegister)
136
		case 0x40: // step-in
137
		case 0x50: // step-in (Update trackRegister)
138
		case 0x60: // step-out
139
		case 0x70: // step-out (Update trackRegister)
140
			startType1Cmd(time);
141
			break;
142
143
		case 0x80: // read sector
144
		case 0x90: // read sector (multi)
145
		case 0xA0: // write sector
146
		case 0xB0: // write sector (multi)
147
			startType2Cmd(time);
148
			break;
149
150
		case 0xC0: // Read Address
151
		case 0xE0: // read track
152
		case 0xF0: // write track
153
			startType3Cmd(time);
154
			break;
155
156
		case 0xD0: // Force interrupt
157
			startType4Cmd(time);
158
			break;
159
	}
160
}
161
162
uint8_t WD2793::getStatusReg(EmuTime time)
163
{
164
	if (((commandReg & 0x80) == 0) || ((commandReg & 0xF0) == 0xD0)) {
165
		// Type I or type IV command
166
		statusReg &= ~(INDEX | TRACK00 | HEAD_LOADED | WRITE_PROTECTED);
167
		if (drive.indexPulse(time)) {
168
			statusReg |=  INDEX;
169
		}
170
		if (drive.isTrack00()) {
171
			statusReg |=  TRACK00;
172
		}
173
		if ((hldTime <= time) && (time < (hldTime + IDLE))) {
174
			statusReg |=  HEAD_LOADED;
175
		}
176
		if (drive.isWriteProtected()) {
177
			statusReg |=  WRITE_PROTECTED;
178
		}
179
	} else {
180
		// Not type I command so bit 1 should be DRQ
181
		if (getDTRQ(time)) {
182
			statusReg |=  S_DRQ;
183
		} else {
184
			statusReg &= ~S_DRQ;
185
		}
186
	}
187
188
	if (isReady()) {
189
		statusReg &= ~NOT_READY;
190
	} else {
191
		statusReg |=  NOT_READY;
192
	}
193
194
	// Reset INTRQ only if it's not scheduled to turn on in the future.
195
	if (irqTime <= time) { // if (INTRQ == true)
196
		irqTime = EmuTime::infinity(); // INTRQ = false;
197
	}
198
199
	return statusReg;
200
}
201
202
uint8_t WD2793::peekStatusReg(EmuTime time) const
203
{
204
	// TODO implement proper peek?
205
	return const_cast<WD2793*>(this)->getStatusReg(time);
206
}
207
208
void WD2793::setTrackReg(uint8_t value, EmuTime /*time*/)
209
{
210
	trackReg = value;
211
}
212
213
uint8_t WD2793::getTrackReg(EmuTime time) const
214
{
215
	return peekTrackReg(time);
216
}
217
218
uint8_t WD2793::peekTrackReg(EmuTime /*time*/) const
219
{
220
	return trackReg;
221
}
222
223
void WD2793::setSectorReg(uint8_t value, EmuTime /*time*/)
224
{
225
	sectorReg = value;
226
}
227
228
uint8_t WD2793::getSectorReg(EmuTime time) const
229
{
230
	return peekSectorReg(time);
231
}
232
233
uint8_t WD2793::peekSectorReg(EmuTime /*time*/) const
234
{
235
	return sectorReg;
236
}
237
238
void WD2793::setDataReg(uint8_t value, EmuTime time)
239
{
240
	dataReg = value;
241
242
	if (!getDTRQ(time)) return;
243
	assert(statusReg & BUSY);
244
245
	if (((commandReg & 0xE0) == 0xA0) || // write sector
246
	    ((commandReg & 0xF0) == 0xF0)) { // write track
247
		dataRegWritten = true;
248
		drqTime.reset(EmuTime::infinity()); // DRQ = false
249
	}
250
}
251
252
uint8_t WD2793::getDataReg(EmuTime time)
253
{
254
	if ((((commandReg & 0xE0) == 0x80) ||   // read sector
255
	     ((commandReg & 0xF0) == 0xC0) ||   // read address
256
	     ((commandReg & 0xF0) == 0xE0)) &&  // read track
257
	    getDTRQ(time)) {
258
		assert(statusReg & BUSY);
259
260
		dataReg = drive.readTrackByte(dataCurrent++);
261
		crc.update(dataReg);
262
		dataAvailable--;
263
		drqTime += 1; // time when the next byte will be available
264
		while (dataAvailable && /*unlikely*/(getDTRQ(time))) {
265
			statusReg |= LOST_DATA;
266
			dataReg = drive.readTrackByte(dataCurrent++);
267
			crc.update(dataReg);
268
			dataAvailable--;
269
			drqTime += 1;
270
		}
271
		assert(!dataAvailable || !getDTRQ(time));
272
		if (dataAvailable == 0) {
273
			if ((commandReg & 0xE0) == 0x80) {
274
				// read sector
275
				// update crc status flag
276
				uint16_t diskCrc  = 256 * drive.readTrackByte(dataCurrent++);
277
				         diskCrc +=       drive.readTrackByte(dataCurrent++);
278
				if (diskCrc == crc.getValue()) {
279
					statusReg &= ~CRC_ERROR;
280
				} else {
281
					statusReg |=  CRC_ERROR;
282
				}
283
				if (sectorInfo.deleted) {
284
					// TODO datasheet isn't clear about this:
285
					// Set this flag at the end of the
286
					// command or as soon as the marker is
287
					// encountered on the disk?
288
					statusReg |= RECORD_TYPE;
289
				}
290
				if (!(commandReg & M_FLAG)) {
291
					endCmd(time);
292
				} else {
293
					// multi sector read, wait for the next sector
294
					drqTime.reset(EmuTime::infinity()); // DRQ = false
295
					sectorReg++;
296
					type2Loaded(time);
297
				}
298
			} else {
299
				// read track, read address
300
				if ((commandReg & 0xF0) == 0xE0) { // read track
301
					drive.invalidateWd2793ReadTrackQuirk();
302
				}
303
				if ((commandReg & 0xF0) == 0xC0) { // read address
304
					if (sectorInfo.addrCrcErr) {
305
						statusReg |=  CRC_ERROR;
306
					} else {
307
						statusReg &= ~CRC_ERROR;
308
					}
309
				}
310
				endCmd(time);
311
			}
312
		}
313
	}
314
	return dataReg;
315
}
316
317
uint8_t WD2793::peekDataReg(EmuTime time) const
318
{
319
	if ((((commandReg & 0xE0) == 0x80) ||   // read sector
320
	     ((commandReg & 0xF0) == 0xC0) ||   // read address
321
	     ((commandReg & 0xF0) == 0xE0)) &&  // read track
322
	    peekDTRQ(time)) {
323
		return drive.readTrackByte(dataCurrent);
324
	} else {
325
		return dataReg;
326
	}
327
}
328
329
330
void WD2793::schedule(FSM state, EmuTime time)
331
{
332
	assert(!isPending());
333
	fsmState = state;
334
	setSyncPoint(time);
335
}
336
337
void WD2793::executeUntil(EmuTime time)
338
{
339
	using enum FSM;
340
	FSM state = fsmState;
341
	fsmState = NONE;
342
	switch (state) {
343
		case SEEK:
344
			if ((commandReg & 0x80) == 0x00) {
345
				// Type I command
346
				seekNext(time);
347
			}
348
			break;
349
		case TYPE2_LOADED:
350
			if ((commandReg & 0xC0) == 0x80)  {
351
				// Type II command
352
				type2Loaded(time);
353
			}
354
			break;
355
		case TYPE2_NOT_FOUND:
356
			if ((commandReg & 0xC0) == 0x80)  {
357
				// Type II command
358
				type2NotFound(time);
359
			}
360
			break;
361
		case TYPE2_ROTATED:
362
			if ((commandReg & 0xC0) == 0x80)  {
363
				// Type II command
364
				type2Rotated(time);
365
			}
366
			break;
367
		case CHECK_WRITE:
368
			if ((commandReg & 0xE0) == 0xA0) {
369
				// write sector command
370
				checkStartWrite(time);
371
			}
372
			break;
373
		case PRE_WRITE_SECTOR:
374
			if ((commandReg & 0xE0) == 0xA0) {
375
				// write sector command
376
				preWriteSector(time);
377
			}
378
			break;
379
		case WRITE_SECTOR:
380
			if ((commandReg & 0xE0) == 0xA0) {
381
				// write sector command
382
				writeSectorData(time);
383
			}
384
			break;
385
		case POST_WRITE_SECTOR:
386
			if ((commandReg & 0xE0) == 0xA0) {
387
				// write sector command
388
				postWriteSector(time);
389
			}
390
			break;
391
		case TYPE3_LOADED:
392
			if (((commandReg & 0xC0) == 0xC0) &&
393
			    ((commandReg & 0xF0) != 0xD0)) {
394
				// Type III command
395
				type3Loaded(time);
396
			}
397
			break;
398
		case TYPE3_ROTATED:
399
			if (((commandReg & 0xC0) == 0xC0) &&
400
			    ((commandReg & 0xF0) != 0xD0)) {
401
				// Type III command
402
				type3Rotated(time);
403
			}
404
			break;
405
		case WRITE_TRACK:
406
			if ((commandReg & 0xF0) == 0xF0) {
407
				// write track command
408
				writeTrackData(time);
409
			}
410
			break;
411
		case READ_TRACK:
412
			if ((commandReg & 0xF0) == 0xE0) {
413
				// read track command
414
				drive.invalidateWd2793ReadTrackQuirk();
415
				endCmd(time); // TODO check this (e.g. DRQ)
416
			}
417
			break;
418
		default:
419
			UNREACHABLE;
420
	}
421
}
422
423
void WD2793::startType1Cmd(EmuTime time)
424
{
425
	statusReg &= ~(SEEK_ERROR | CRC_ERROR);
426
	statusReg |= BUSY;
427
428
	if (commandReg & H_FLAG) {
429
		// Activate HLD, WD2793 now waits for the HLT response. But on
430
		// all MSX machines I checked HLT is just stubbed to +5V. So
431
		// from a WD2793 point of view the head is loaded immediately.
432
		hldTime = time;
433
	} else {
434
		// deactivate HLD
435
		hldTime = EmuTime::infinity();
436
	}
437
438
	switch (commandReg & 0xF0) {
439
		case 0x00: // restore
440
			trackReg = 0xFF;
441
			dataReg  = 0x00;
442
			seek(time);
443
			break;
444
445
		case 0x10: // seek
446
			seek(time);
447
			break;
448
449
		case 0x20: // step
450
		case 0x30: // step (Update trackRegister)
451
			step(time);
452
			break;
453
454
		case 0x40: // step-in
455
		case 0x50: // step-in (Update trackRegister)
456
			directionIn = true;
457
			step(time);
458
			break;
459
460
		case 0x60: // step-out
461
		case 0x70: // step-out (Update trackRegister)
462
			directionIn = false;
463
			step(time);
464
			break;
465
	}
466
}
467
468
void WD2793::seek(EmuTime time)
469
{
470
	if (trackReg == dataReg) {
471
		endType1Cmd(time);
472
	} else {
473
		directionIn = (dataReg > trackReg);
474
		step(time);
475
	}
476
}
477
478
void WD2793::step(EmuTime time)
479
{
480
	static constexpr std::array<EmuDuration, 4> timePerStep = {
481
		// in case a 1MHz clock is used (as in MSX)
482
		EmuDuration::msec( 6),
483
		EmuDuration::msec(12),
484
		EmuDuration::msec(20),
485
		EmuDuration::msec(30),
486
	};
487
488
	if ((commandReg & T_FLAG) || ((commandReg & 0xE0) == 0x00)) {
489
		// Restore or seek  or  T_FLAG
490
		if (directionIn) {
491
			trackReg++;
492
		} else {
493
			trackReg--;
494
		}
495
	}
496
	if (!directionIn && drive.isTrack00()) {
497
		trackReg = 0;
498
		endType1Cmd(time);
499
	} else {
500
		drive.step(directionIn, time);
501
		schedule(FSM::SEEK, time + timePerStep[commandReg & STEP_SPEED]);
502
	}
503
}
504
505
void WD2793::seekNext(EmuTime time)
506
{
507
	if ((commandReg & 0xE0) == 0x00) {
508
		// Restore or seek
509
		seek(time);
510
	} else {
511
		endType1Cmd(time);
512
	}
513
}
514
515
void WD2793::endType1Cmd(EmuTime time)
516
{
517
	if (commandReg & V_FLAG) {
518
		// verify sequence
519
		// TODO verify sequence
520
	}
521
	endCmd(time);
522
}
523
524
525
void WD2793::startType2Cmd(EmuTime time)
526
{
527
	statusReg &= ~(LOST_DATA   | RECORD_NOT_FOUND |
528
	               RECORD_TYPE | WRITE_PROTECTED);
529
	statusReg |= BUSY;
530
	dataRegWritten = false;
531
532
	if (!isReady()) {
533
		endCmd(time);
534
	} else {
535
		// WD2795/WD2797 would now set SSO output
536
		hldTime = time; // see comment in startType1Cmd
537
538
		if (commandReg & E_FLAG) {
539
			schedule(FSM::TYPE2_LOADED,
540
			         time + EmuDuration::msec(30)); // when 1MHz clock
541
		} else {
542
			type2Loaded(time);
543
		}
544
	}
545
}
546
547
void WD2793::type2Loaded(EmuTime time)
548
{
549
	if (((commandReg & 0xE0) == 0xA0) && (drive.isWriteProtected())) {
550
		// write command and write protected
551
		statusReg |= WRITE_PROTECTED;
552
		endCmd(time);
553
		return;
554
	}
555
556
	pulse5 = drive.getTimeTillIndexPulse(time, 5);
557
	type2Search(time);
558
}
559
560
void WD2793::type2Search(EmuTime time)
561
{
562
	assert(time < pulse5);
563
	// Locate (next) sector on disk.
564
	try {
565
		setDrqRate(drive.getTrackLength());
566
		EmuTime next = drive.getNextSector(time, sectorInfo);
567
		if (next < pulse5) {
568
			// Wait till sector is actually rotated under head
569
			schedule(FSM::TYPE2_ROTATED, next);
570
			return;
571
		}
572
	} catch (MSXException& /*e*/) {
573
		// nothing
574
	}
575
	// Sector not found in 5 revolutions (or read error),
576
	// schedule to give a RECORD_NOT_FOUND error
577
	if (pulse5 < EmuTime::infinity()) {
578
		schedule(FSM::TYPE2_NOT_FOUND, pulse5);
579
	} else {
580
		// Drive not rotating. How does a real WD293 handle this?
581
		type2NotFound(time);
582
	}
583
}
584
585
void WD2793::type2Rotated(EmuTime time)
586
{
587
	// The CRC status bit should only toggle after the disk has rotated
588
	if (sectorInfo.addrCrcErr) {
589
		statusReg |=  CRC_ERROR;
590
	} else {
591
		statusReg &= ~CRC_ERROR;
592
	}
593
	if ((sectorInfo.addrCrcErr) ||
594
	    (sectorInfo.track  != trackReg) ||
595
	    (sectorInfo.sector != sectorReg)) {
596
		// TODO implement (optional) head compare
597
		// not the sector we were looking for, continue searching
598
		type2Search(time);
599
		return;
600
	}
601
	if (sectorInfo.dataIdx == -1) {
602
		// Sector header without accompanying data block.
603
		// TODO we should actually wait for the disk to rotate before
604
		// we can check this.
605
		type2Search(time);
606
		return;
607
	}
608
609
	// Ok, found matching sector.
610
	switch (commandReg & 0xE0) {
611
	case 0x80: // read sector  or  read sector multi
612
		startReadSector(time);
613
		break;
614
615
	case 0xA0: // write sector  or  write sector multi
616
		startWriteSector(time);
617
		break;
618
	}
619
}
620
621
void WD2793::type2NotFound(EmuTime time)
622
{
623
	statusReg |= RECORD_NOT_FOUND;
624
	endCmd(time);
625
}
626
627
void WD2793::startReadSector(EmuTime time)
628
{
629
	if (sectorInfo.deleted) {
630
		crc.init({0xA1, 0xA1, 0xA1, 0xF8});
631
	} else {
632
		crc.init({0xA1, 0xA1, 0xA1, 0xFB});
633
	}
634
	unsigned trackLength = drive.getTrackLength();
635
	int tmp = sectorInfo.dataIdx - sectorInfo.addrIdx;
636
	unsigned gapLength = (tmp >= 0) ? tmp : (tmp + trackLength);
637
	assert(gapLength < trackLength);
638
	drqTime.reset(time);
639
	drqTime += gapLength + 1 + 1; // (first) byte can be read in a moment
640
	dataCurrent = sectorInfo.dataIdx;
641
642
	// Get sector size from disk: 128, 256, 512 or 1024 bytes
643
	// Verified on real WD2793:
644
	//   size-code = 255 results in a sector size of 1024 bytes,
645
	// This suggests the WD2793 only looks at the lower 2 bits.
646
	dataAvailable = 128 << (sectorInfo.sizeCode & 3);
647
}
648
649
void WD2793::startWriteSector(EmuTime time)
650
{
651
	// At the current moment in time, the 'FE' byte in the address mark
652
	// is located under the drive head (because the DMK format points to
653
	// the 'FE' byte in the address header). After this byte there still
654
	// follow the C,H,R,N and 2 crc bytes. So the address header ends in
655
	// 7 bytes.
656
	// - After 2 more bytes the WD2793 will activate DRQ.
657
	// - 8 bytes later the WD2793 will check that the CPU has send the
658
	//   first byte (if not the command will be aborted without any writes
659
	//   to the disk, not even gap or data mark bytes).
660
	// - after a pause of 12 bytes, the WD2793 will write 12 zero bytes,
661
	//   followed by the 4 bytes data header (A1 A1 A1 FB).
662
	// - Next the WD2793 write the actual data bytes. At this moment it
663
	//   will also activate DRQ to receive the 2nd byte from the CPU.
664
	//
665
	// Note that between the 1st and 2nd activation of DRQ is a longer
666
	// duration than between all later DRQ activations. The write-sector
667
	// routine in Microsol_CDX-2 depends on this.
668
669
	drqTime.reset(time);
670
	drqTime += 7 + 2; // activate DRQ 2 bytes after end of address header
671
672
	// 8 bytes later, the WD2793 will check whether the CPU wrote the
673
	// first byte.
674
	schedule(FSM::CHECK_WRITE, drqTime + 8);
675
}
676
677
void WD2793::checkStartWrite(EmuTime time)
678
{
679
	// By now the CPU should already have written the first byte, otherwise
680
	// the write sector command doesn't even start.
681
	if (!dataRegWritten) {
682
		statusReg |= LOST_DATA;
683
		endCmd(time);
684
		return;
685
	}
686
687
	// From this point onwards the FDC will write a full sector to the disk
688
	// (including markers and CRC). If the CPU doesn't supply data bytes in
689
	// a timely manner, the missing bytes are filled with zero.
690
	//
691
	// But it is possible to cancel the write sector command to only write
692
	// a partial sector (e.g. without CRC bytes). See cbsfox' comment in
693
	// this forum thread:
694
	//    https://www.msx.org/forum/msx-talk/software/pdi-to-dmk-using-dsk-pro-104-with-openmsx
695
	dataCurrent = sectorInfo.addrIdx
696
	            + 6 // C H R N CRC1 CRC2
697
		    + 22;
698
699
	// pause 12 bytes
700
	drqTime.reset(time);
701
	schedule(FSM::PRE_WRITE_SECTOR, drqTime + 12);
702
	drqTime.reset(EmuTime::infinity()); // DRQ = false
703
	dataAvailable = 16; // 12+4 bytes pre-data
704
}
705
706
void WD2793::preWriteSector(EmuTime time)
707
{
708
	try {
709
		--dataAvailable;
710
		if (dataAvailable > 0) {
711
			if (dataAvailable >= 4) {
712
				// write 12 zero-bytes
713
				drive.writeTrackByte(dataCurrent++, 0x00);
714
			} else {
715
				// followed by 3x A1-bytes
716
				drive.writeTrackByte(dataCurrent++, 0xA1);
717
			}
718
			drqTime.reset(time);
719
			schedule(FSM::PRE_WRITE_SECTOR, drqTime + 1);
720
			drqTime.reset(EmuTime::infinity()); // DRQ = false
721
		} else {
722
			// and finally a single F8/FB byte
723
			crc.init({0xA1, 0xA1, 0xA1});
724
			uint8_t mark = (commandReg & A0_FLAG) ? 0xF8 : 0xFB;
725
			drive.writeTrackByte(dataCurrent++, mark);
726
			crc.update(mark);
727
728
			// Pre-data is finished. Next start writing the actual data bytes
729
			dataOutReg = dataReg;
730
			dataRegWritten = false;
731
			dataAvailable = 128 << (sectorInfo.sizeCode & 3); // see comment in startReadSector()
732
733
			// Re-activate DRQ
734
			drqTime.reset(time);
735
736
			// Moment in time when first data byte gets written
737
			schedule(FSM::WRITE_SECTOR, drqTime + 1);
738
		}
739
	} catch (MSXException&) {
740
		statusReg |= NOT_READY; // TODO which status bit should be set?
741
		endCmd(time);
742
	}
743
}
744
745
void WD2793::writeSectorData(EmuTime time)
746
{
747
	try {
748
		// Write data byte
749
		drive.writeTrackByte(dataCurrent++, dataOutReg);
750
		crc.update(dataOutReg);
751
		--dataAvailable;
752
753
		if (dataAvailable > 0) {
754
			if (dataRegWritten) {
755
				dataOutReg = dataReg;
756
				dataRegWritten = false;
757
			} else {
758
				dataOutReg = 0;
759
				statusReg |= LOST_DATA;
760
			}
761
			// Re-activate DRQ
762
			drqTime.reset(time);
763
764
			// Moment in time when next data byte gets written
765
			schedule(FSM::WRITE_SECTOR, drqTime + 1);
766
767
			if (dataAvailable == 1) {
768
				// Don't reactivate DRQ if only the last
769
				// data-byte still needs to be written to disk.
770
				// At this point the CPU has already send that
771
				// last byte.
772
				drqTime.reset(EmuTime::infinity()); // DRQ = false
773
			}
774
		} else {
775
			// Next write post-part
776
			dataAvailable = 3;
777
			drqTime.reset(time);
778
			schedule(FSM::POST_WRITE_SECTOR, drqTime + 1);
779
			drqTime.reset(EmuTime::infinity()); // DRQ = false
780
		}
781
	} catch (MSXException&) {
782
		statusReg |= NOT_READY; // TODO which status bit should be set?
783
		endCmd(time);
784
	}
785
}
786
787
void WD2793::postWriteSector(EmuTime time)
788
{
789
	try {
790
		--dataAvailable;
791
		if (dataAvailable > 0) {
792
			// write 2 CRC bytes (big endian)
793
			uint8_t val = (dataAvailable == 2) ? narrow_cast<uint8_t>(crc.getValue() >> 8)
794
			                                   : narrow_cast<uint8_t>(crc.getValue() & 0xFF);
795
			drive.writeTrackByte(dataCurrent++, val);
796
			drqTime.reset(time);
797
			schedule(FSM::POST_WRITE_SECTOR, drqTime + 1);
798
			drqTime.reset(EmuTime::infinity()); // DRQ = false
799
		} else {
800
			// write one byte of 0xFE
801
			drive.writeTrackByte(dataCurrent++, 0xFE);
802
803
			// flush sector (actually full track) to disk.
804
			drive.flushTrack();
805
806
			if (!(commandReg & M_FLAG)) {
807
				endCmd(time);
808
			} else {
809
				// multi sector write, wait for next sector
810
				drqTime.reset(EmuTime::infinity()); // DRQ = false
811
				sectorReg++;
812
				type2Loaded(time);
813
			}
814
		}
815
	} catch (MSXException&) {
816
		// e.g. triggers when a different drive was selected during write
817
		statusReg |= NOT_READY; // TODO which status bit should be set?
818
		endCmd(time);
819
	}
820
}
821
822
823
void WD2793::startType3Cmd(EmuTime time)
824
{
825
	statusReg &= ~(LOST_DATA | RECORD_NOT_FOUND | RECORD_TYPE);
826
	statusReg |= BUSY;
827
828
	if (!isReady()) {
829
		endCmd(time);
830
	} else {
831
		if ((commandReg & 0xF0) == 0xF0) { // write track
832
			// immediately activate DRQ
833
			drqTime.reset(time); // DRQ = true
834
		}
835
836
		hldTime = time; // see comment in startType1Cmd
837
		// WD2795/WD2797 would now set SSO output
838
839
		if (commandReg & E_FLAG) {
840
			schedule(FSM::TYPE3_LOADED,
841
			         time + EmuDuration::msec(30)); // when 1MHz clock
842
		} else {
843
			type3Loaded(time);
844
		}
845
	}
846
}
847
848
void WD2793::type3Loaded(EmuTime time)
849
{
850
	// TODO TG43 update
851
	if (((commandReg & 0xF0) == 0xF0) && (drive.isWriteProtected())) {
852
		// write track command and write protected
853
		statusReg |= WRITE_PROTECTED;
854
		endCmd(time);
855
		return;
856
	}
857
858
	EmuTime next(EmuTime::dummy());
859
	if ((commandReg & 0xF0) == 0xC0) {
860
		// read address
861
		try {
862
			// wait till next sector header
863
			setDrqRate(drive.getTrackLength());
864
			next = drive.getNextSector(time, sectorInfo);
865
			if (next == EmuTime::infinity()) {
866
				// TODO wait for 5 revolutions
867
				statusReg |= RECORD_NOT_FOUND;
868
				endCmd(time);
869
				return;
870
			}
871
			dataCurrent = sectorInfo.addrIdx;
872
			dataAvailable = 6;
873
			sectorReg = drive.readTrackByte(dataCurrent);
874
		} catch (MSXException&) {
875
			// read addr failed
876
			statusReg |= RECORD_NOT_FOUND;
877
			endCmd(time);
878
			return;
879
		}
880
	} else {
881
		// read/write track
882
		// wait till next index pulse
883
		next = drive.getTimeTillIndexPulse(time);
884
		if (next == EmuTime::infinity()) {
885
			// drive became not ready since the command was started,
886
			// how does a real WD2793 handle this?
887
			endCmd(time);
888
			return;
889
		}
890
	}
891
	schedule(FSM::TYPE3_ROTATED, next);
892
}
893
894
void WD2793::type3Rotated(EmuTime time)
895
{
896
	switch (commandReg & 0xF0) {
897
	case 0xC0: // read Address
898
		readAddressCmd(time);
899
		break;
900
	case 0xE0: // read track
901
		readTrackCmd(time);
902
		break;
903
	case 0xF0: // write track
904
		startWriteTrack(time);
905
		break;
906
	}
907
}
908
909
void WD2793::readAddressCmd(EmuTime time)
910
{
911
	drqTime.reset(time);
912
	drqTime += 1; // (first) byte can be read in a moment
913
}
914
915
void WD2793::readTrackCmd(EmuTime time)
916
{
917
	try {
918
		unsigned trackLength = drive.getTrackLength();
919
		drive.applyWd2793ReadTrackQuirk();
920
		setDrqRate(trackLength);
921
		dataCurrent = 0;
922
		dataAvailable = narrow<int>(trackLength);
923
		drqTime.reset(time);
924
925
		// Stop command at next index pulse
926
		schedule(FSM::READ_TRACK, drqTime + dataAvailable);
927
928
		drqTime += 1; // (first) byte can be read in a moment
929
	} catch (MSXException&) {
930
		// read track failed, TODO status bits?
931
		drive.invalidateWd2793ReadTrackQuirk();
932
		endCmd(time);
933
	}
934
}
935
936
void WD2793::startWriteTrack(EmuTime time)
937
{
938
	// By now the CPU should already have written the first byte, otherwise
939
	// the write track command doesn't even start.
940
	if (!dataRegWritten) {
941
		statusReg |= LOST_DATA;
942
		endCmd(time);
943
		return;
944
	}
945
	try {
946
		unsigned trackLength = drive.getTrackLength();
947
		setDrqRate(trackLength);
948
		dataCurrent = 0;
949
		dataAvailable = narrow<int>(trackLength);
950
		lastWasA1 = false;
951
		lastWasCRC = false;
952
		dataOutReg = dataReg;
953
		dataRegWritten = false;
954
		drqTime.reset(time); // DRQ = true
955
956
		// Moment in time when first track byte gets written
957
		schedule(FSM::WRITE_TRACK, drqTime + 1);
958
	} catch (MSXException& /*e*/) {
959
		endCmd(time);
960
	}
961
}
962
963
void WD2793::writeTrackData(EmuTime time)
964
{
965
	try {
966
		bool prevA1 = lastWasA1;
967
		lastWasA1 = false;
968
969
		// handle chars with special meaning
970
		uint8_t CRCvalue2 = 0; // dummy
971
		bool idam = false;
972
		if (lastWasCRC) {
973
			// 2nd CRC byte, don't transform
974
			lastWasCRC = false;
975
		} else if (dataOutReg == 0xF5) {
976
			// write A1 with missing clock transitions
977
			dataOutReg = 0xA1;
978
			lastWasA1 = true;
979
			// Initialize CRC: the calculated CRC value
980
			// includes the 3 A1 bytes. So when starting
981
			// from the initial value 0xffff, we should not
982
			// re-initialize the CRC value on the 2nd and
983
			// 3rd A1 byte. Though what we do instead is on
984
			// each A1 byte initialize the value as if
985
			// there were already 2 A1 bytes written.
986
			crc.init({0xA1, 0xA1});
987
		} else if (dataOutReg == 0xF6) {
988
			// write C2 with missing clock transitions
989
			dataOutReg = 0xC2;
990
		} else if (dataOutReg == 0xF7) {
991
			// write 2 CRC bytes, big endian
992
			dataOutReg = narrow_cast<uint8_t>(crc.getValue() >> 8); // high byte
993
			CRCvalue2  = narrow_cast<uint8_t>(crc.getValue() >> 0); // low byte
994
			lastWasCRC = true;
995
		} else if (dataOutReg == 0xFE) {
996
			// Record locations of 0xA1 (with missing clock
997
			// transition) followed by 0xFE. The FE byte has
998
			// no special meaning for the WD2793 itself,
999
			// but it does for the DMK file format.
1000
			if (prevA1) idam = true;
1001
		}
1002
		// actually write (transformed) byte
1003
		drive.writeTrackByte(dataCurrent++, dataOutReg, idam);
1004
		crc.update(dataOutReg); // also when 'lastWasCRC == true'
1005
		--dataAvailable;
1006
1007
		if (dataAvailable > 0) {
1008
			drqTime.reset(time); // DRQ = true
1009
1010
			// Moment in time when next track byte gets written
1011
			schedule(FSM::WRITE_TRACK, drqTime + 1);
1012
1013
			// prepare next byte
1014
			if (!lastWasCRC) {
1015
				if (dataRegWritten) {
1016
					dataOutReg = dataReg;
1017
					dataRegWritten = false;
1018
				} else {
1019
					dataOutReg = 0;
1020
					statusReg |= LOST_DATA;
1021
				}
1022
			} else {
1023
				dataOutReg = CRCvalue2;
1024
				// don't re-activate DRQ for 2nd byte of CRC
1025
				drqTime.reset(EmuTime::infinity()); // DRQ = false
1026
			}
1027
		} else {
1028
			// Write track done
1029
			drive.flushTrack();
1030
			endCmd(time);
1031
		}
1032
	} catch (MSXException&) {
1033
		statusReg |= NOT_READY; // TODO which status bit should be set?
1034
		endCmd(time);
1035
	}
1036
}
1037
1038
void WD2793::startType4Cmd(EmuTime time)
1039
{
1040
	// Force interrupt
1041
	uint8_t flags = commandReg & 0x0F;
1042
	if (flags & (N2R_IRQ | R2N_IRQ)) {
1043
		// all flags not yet supported
1044
		#ifdef DEBUG
1045
		std::cerr << "WD2793 type 4 cmd, unimplemented bits " << int(flags) << '\n';
1046
		#endif
1047
	}
1048
1049
	if (flags == 0x00) {
1050
		immediateIRQ = false;
1051
	}
1052
	if ((flags & IDX_IRQ) && isReady()) {
1053
		irqTime = drive.getTimeTillIndexPulse(time);
1054
	} else {
1055
		assert(irqTime == EmuTime::infinity()); // INTRQ = false
1056
	}
1057
	if (flags & IMM_IRQ) {
1058
		immediateIRQ = true;
1059
	}
1060
1061
	drqTime.reset(EmuTime::infinity()); // DRQ = false
1062
	statusReg &= ~BUSY; // reset status on Busy
1063
}
1064
1065
void WD2793::endCmd(EmuTime time)
1066
{
1067
	if ((hldTime <= time) && (time < (hldTime + IDLE))) {
1068
		// HLD was active, start timeout period
1069
		// Real WD2793 waits for 15 index pulses. We approximate that
1070
		// here by waiting for 3s.
1071
		hldTime = time;
1072
	}
1073
	drqTime.reset(EmuTime::infinity()); // DRQ   = false
1074
	irqTime = EmuTime::zero();          // INTRQ = true;
1075
	statusReg &= ~BUSY;
1076
}
1077
1078
1079
static constexpr auto fsmStateInfo = std::to_array<enum_string<WD2793::FSM>>({
1080
	{ "NONE",              WD2793::FSM::NONE },
1081
	{ "SEEK",              WD2793::FSM::SEEK },
1082
	{ "TYPE2_LOADED",      WD2793::FSM::TYPE2_LOADED },
1083
	{ "TYPE2_NOT_FOUND",   WD2793::FSM::TYPE2_NOT_FOUND },
1084
	{ "TYPE2_ROTATED",     WD2793::FSM::TYPE2_ROTATED },
1085
	{ "CHECK_WRITE",       WD2793::FSM::CHECK_WRITE },
1086
	{ "PRE_WRITE_SECTOR",  WD2793::FSM::PRE_WRITE_SECTOR },
1087
	{ "WRITE_SECTOR",      WD2793::FSM::WRITE_SECTOR },
1088
	{ "POST_WRITE_SECTOR", WD2793::FSM::POST_WRITE_SECTOR },
1089
	{ "TYPE3_LOADED",      WD2793::FSM::TYPE3_LOADED },
1090
	{ "TYPE3_ROTATED",     WD2793::FSM::TYPE3_ROTATED },
1091
	{ "WRITE_TRACK",       WD2793::FSM::WRITE_TRACK },
1092
	{ "READ_TRACK",        WD2793::FSM::READ_TRACK },
1093
	{ "IDX_IRQ",           WD2793::FSM::IDX_IRQ },
1094
	// for bw-compat savestate
1095
	{ "TYPE2_WAIT_LOAD",   WD2793::FSM::TYPE2_LOADED }, // was FSM::TYPE2_WAIT_LOAD
1096
	{ "TYPE3_WAIT_LOAD",   WD2793::FSM::TYPE3_LOADED }, // was FSM::TYPE3_WAIT_LOAD
1097
});
1098
SERIALIZE_ENUM(WD2793::FSM, fsmStateInfo);
1099
1100
// version 1: initial version
1101
// version 2: removed members: commandStart, DRQTimer, DRQ, transferring, formatting
1102
//            added member: drqTime (has different semantics than DRQTimer)
1103
//            also the timing of the data-transfer commands (read/write sector
1104
//            and write track) has changed. So this could result in replay-sync
1105
//            errors.
1106
//            (Also the enum FSMState has changed, but that's not a problem.)
1107
// version 3: Added members 'crc' and 'lastWasA1'.
1108
//            Replaced 'dataBuffer' with 'trackData'. We don't attempt to migrate
1109
//            the old 'dataBuffer' content to 'trackData' (doing so would be
1110
//            quite difficult). This means that old savestates that were in the
1111
//            middle of a sector/track read/write command probably won't work
1112
//            correctly anymore. We do give a warning on this.
1113
// version 4: changed type of drqTime from Clock to DynamicClock
1114
// version 5: added 'pulse5' and 'sectorInfo'
1115
// version 6: no layout changes, only added new enum value 'FSM::CHECK_WRITE'
1116
// version 7: replaced 'bool INTRQ' with 'EmuTime irqTime'
1117
// version 8: removed 'userData' from Schedulable
1118
// version 9: added 'trackDataValid'
1119
// version 10: removed 'trackData' and 'trackDataValid' (moved to RealDrive)
1120
// version 11: added 'dataOutReg', 'dataRegWritten', 'lastWasCRC'
1121
// version 12: added 'hldTime'
1122
template<typename Archive>
1123
void WD2793::serialize(Archive& ar, unsigned version)
1124
{
1125
	EmuTime bw_irqTime = EmuTime::zero();
1126
	if (ar.versionAtLeast(version, 8)) {
1127
		ar.template serializeBase<Schedulable>(*this);
1128
	} else {
1129
		constexpr int SCHED_FSM     = 0;
1130
		constexpr int SCHED_IDX_IRQ = 1;
1131
		assert(Archive::IS_LOADER);
1132
		removeSyncPoint();
1133
		for (auto& old : Schedulable::serializeBW(ar)) {
1134
			if (old.userData == SCHED_FSM) {
1135
				setSyncPoint(old.time);
1136
			} else if (old.userData == SCHED_IDX_IRQ) {
1137
				bw_irqTime = old.time;
1138
			}
1139
		}
1140
	}
1141
1142
	ar.serialize("fsmState",      fsmState,
1143
	             "statusReg",     statusReg,
1144
	             "commandReg",    commandReg,
1145
	             "sectorReg",     sectorReg,
1146
	             "trackReg",      trackReg,
1147
	             "dataReg",       dataReg,
1148
1149
	             "directionIn",   directionIn,
1150
	             "immediateIRQ",  immediateIRQ,
1151
1152
	             "dataCurrent",   dataCurrent,
1153
	             "dataAvailable", dataAvailable);
1154
1155
	if (ar.versionAtLeast(version, 2)) {
1156
		if (ar.versionAtLeast(version, 4)) {
1157
			ar.serialize("drqTime", drqTime);
1158
		} else {
1159
			assert(Archive::IS_LOADER);
1160
			Clock<6250 * 5> c(EmuTime::dummy());
1161
			ar.serialize("drqTime", c);
1162
			drqTime.reset(c.getTime());
1163
			drqTime.setFreq(6250 * 5);
1164
		}
1165
	} else {
1166
		assert(Archive::IS_LOADER);
1167
		//ar.serialize("commandStart", commandStart,
1168
		//             "DRQTimer",     DRQTimer,
1169
		//             "DRQ",          DRQ,
1170
		//             "transferring", transferring,
1171
		//             "formatting",   formatting);
1172
		drqTime.reset(EmuTime::infinity());
1173
	}
1174
1175
	if (ar.versionAtLeast(version, 3)) {
1176
		ar.serialize("lastWasA1", lastWasA1);
1177
		uint16_t crcVal = crc.getValue();
1178
		ar.serialize("crc", crcVal);
1179
		crc.init(crcVal);
1180
	}
1181
1182
	if (ar.versionAtLeast(version, 5)) {
1183
		ar.serialize("pulse5",     pulse5,
1184
		             "sectorInfo", sectorInfo);
1185
	} else {
1186
		// leave pulse5 at EmuTime::infinity()
1187
		// leave sectorInfo uninitialized
1188
	}
1189
1190
	if (ar.versionAtLeast(version, 7)) {
1191
		ar.serialize("irqTime", irqTime);
1192
	} else {
1193
		assert(Archive::IS_LOADER);
1194
		bool INTRQ = false; // dummy init to avoid warning
1195
		ar.serialize("INTRQ", INTRQ);
1196
		irqTime = INTRQ ? EmuTime::zero() : EmuTime::infinity();
1197
		if (bw_irqTime != EmuTime::zero()) {
1198
			irqTime = bw_irqTime;
1199
		}
1200
	}
1201
1202
	if (ar.versionAtLeast(version, 11)) {
1203
		ar.serialize("dataOutReg",     dataOutReg,
1204
		             "dataRegWritten", dataRegWritten,
1205
		             "lastWasCRC",     lastWasCRC);
1206
	} else {
1207
		assert(Archive::IS_LOADER);
1208
		dataOutReg = dataReg;
1209
		dataRegWritten = false;
1210
		lastWasCRC = false;
1211
	}
1212
1213
	if (ar.versionBelow(version, 11)) {
1214
		assert(Archive::IS_LOADER);
1215
		// version 9->10: 'trackData' moved from FDC to RealDrive
1216
		// version 10->11: write commands are different
1217
		if (statusReg & BUSY) {
1218
			cliComm.printWarning(
1219
				"Loading an old savestate that has an "
1220
				"in-progress WD2793 command. This is not "
1221
				"fully backwards-compatible and can cause "
1222
				"wrong emulation behavior.");
1223
		}
1224
	}
1225
1226
	if (ar.versionAtLeast(version, 12)) {
1227
		ar.serialize("hldTime", hldTime);
1228
	} else {
1229
		if (statusReg & BUSY) {
1230
			hldTime = getCurrentTime();
1231
		} else {
1232
			hldTime = EmuTime::infinity();
1233
		}
1234
	}
1235
}
1236
INSTANTIATE_SERIALIZE_METHODS(WD2793);
1237
1238
} // namespace openmsx