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