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(FSM state, EmuTime::param time)
328{
329 assert(!pendingSyncPoint());
330 fsmState = state;
331 setSyncPoint(time);
332}
333
334void WD2793::executeUntil(EmuTime::param time)
335{
336 using enum FSM;
337 FSM state = fsmState;
338 fsmState = NONE;
339 switch (state) {
340 case SEEK:
341 if ((commandReg & 0x80) == 0x00) {
342 // Type I command
343 seekNext(time);
344 }
345 break;
346 case TYPE2_LOADED:
347 if ((commandReg & 0xC0) == 0x80) {
348 // Type II command
349 type2Loaded(time);
350 }
351 break;
352 case TYPE2_NOT_FOUND:
353 if ((commandReg & 0xC0) == 0x80) {
354 // Type II command
355 type2NotFound(time);
356 }
357 break;
358 case TYPE2_ROTATED:
359 if ((commandReg & 0xC0) == 0x80) {
360 // Type II command
361 type2Rotated(time);
362 }
363 break;
364 case CHECK_WRITE:
365 if ((commandReg & 0xE0) == 0xA0) {
366 // write sector command
367 checkStartWrite(time);
368 }
369 break;
370 case PRE_WRITE_SECTOR:
371 if ((commandReg & 0xE0) == 0xA0) {
372 // write sector command
373 preWriteSector(time);
374 }
375 break;
376 case WRITE_SECTOR:
377 if ((commandReg & 0xE0) == 0xA0) {
378 // write sector command
379 writeSectorData(time);
380 }
381 break;
383 if ((commandReg & 0xE0) == 0xA0) {
384 // write sector command
385 postWriteSector(time);
386 }
387 break;
388 case TYPE3_LOADED:
389 if (((commandReg & 0xC0) == 0xC0) &&
390 ((commandReg & 0xF0) != 0xD0)) {
391 // Type III command
392 type3Loaded(time);
393 }
394 break;
395 case TYPE3_ROTATED:
396 if (((commandReg & 0xC0) == 0xC0) &&
397 ((commandReg & 0xF0) != 0xD0)) {
398 // Type III command
399 type3Rotated(time);
400 }
401 break;
402 case WRITE_TRACK:
403 if ((commandReg & 0xF0) == 0xF0) {
404 // write track command
405 writeTrackData(time);
406 }
407 break;
408 case READ_TRACK:
409 if ((commandReg & 0xF0) == 0xE0) {
410 // read track command
412 endCmd(time); // TODO check this (e.g. DRQ)
413 }
414 break;
415 default:
417 }
418}
419
420void WD2793::startType1Cmd(EmuTime::param time)
421{
422 statusReg &= ~(SEEK_ERROR | CRC_ERROR);
423 statusReg |= BUSY;
424
425 if (commandReg & H_FLAG) {
426 // Activate HLD, WD2793 now waits for the HLT response. But on
427 // all MSX machines I checked HLT is just stubbed to +5V. So
428 // from a WD2793 point of view the head is loaded immediately.
429 hldTime = time;
430 } else {
431 // deactivate HLD
432 hldTime = EmuTime::infinity();
433 }
434
435 switch (commandReg & 0xF0) {
436 case 0x00: // restore
437 trackReg = 0xFF;
438 dataReg = 0x00;
439 seek(time);
440 break;
441
442 case 0x10: // seek
443 seek(time);
444 break;
445
446 case 0x20: // step
447 case 0x30: // step (Update trackRegister)
448 step(time);
449 break;
450
451 case 0x40: // step-in
452 case 0x50: // step-in (Update trackRegister)
453 directionIn = true;
454 step(time);
455 break;
456
457 case 0x60: // step-out
458 case 0x70: // step-out (Update trackRegister)
459 directionIn = false;
460 step(time);
461 break;
462 }
463}
464
465void WD2793::seek(EmuTime::param time)
466{
467 if (trackReg == dataReg) {
468 endType1Cmd(time);
469 } else {
470 directionIn = (dataReg > trackReg);
471 step(time);
472 }
473}
474
475void WD2793::step(EmuTime::param time)
476{
477 static constexpr std::array<EmuDuration, 4> timePerStep = {
478 // in case a 1MHz clock is used (as in MSX)
483 };
484
485 if ((commandReg & T_FLAG) || ((commandReg & 0xE0) == 0x00)) {
486 // Restore or seek or T_FLAG
487 if (directionIn) {
488 trackReg++;
489 } else {
490 trackReg--;
491 }
492 }
493 if (!directionIn && drive.isTrack00()) {
494 trackReg = 0;
495 endType1Cmd(time);
496 } else {
497 drive.step(directionIn, time);
498 schedule(FSM::SEEK, time + timePerStep[commandReg & STEP_SPEED]);
499 }
500}
501
502void WD2793::seekNext(EmuTime::param time)
503{
504 if ((commandReg & 0xE0) == 0x00) {
505 // Restore or seek
506 seek(time);
507 } else {
508 endType1Cmd(time);
509 }
510}
511
512void WD2793::endType1Cmd(EmuTime::param time)
513{
514 if (commandReg & V_FLAG) {
515 // verify sequence
516 // TODO verify sequence
517 }
518 endCmd(time);
519}
520
521
522void WD2793::startType2Cmd(EmuTime::param time)
523{
524 statusReg &= ~(LOST_DATA | RECORD_NOT_FOUND |
525 RECORD_TYPE | WRITE_PROTECTED);
526 statusReg |= BUSY;
527 dataRegWritten = false;
528
529 if (!isReady()) {
530 endCmd(time);
531 } else {
532 // WD2795/WD2797 would now set SSO output
533 hldTime = time; // see comment in startType1Cmd
534
535 if (commandReg & E_FLAG) {
536 schedule(FSM::TYPE2_LOADED,
537 time + EmuDuration::msec(30)); // when 1MHz clock
538 } else {
539 type2Loaded(time);
540 }
541 }
542}
543
544void WD2793::type2Loaded(EmuTime::param time)
545{
546 if (((commandReg & 0xE0) == 0xA0) && (drive.isWriteProtected())) {
547 // write command and write protected
548 statusReg |= WRITE_PROTECTED;
549 endCmd(time);
550 return;
551 }
552
553 pulse5 = drive.getTimeTillIndexPulse(time, 5);
554 type2Search(time);
555}
556
557void WD2793::type2Search(EmuTime::param time)
558{
559 assert(time < pulse5);
560 // Locate (next) sector on disk.
561 try {
562 setDrqRate(drive.getTrackLength());
563 EmuTime next = drive.getNextSector(time, sectorInfo);
564 if (next < pulse5) {
565 // Wait till sector is actually rotated under head
566 schedule(FSM::TYPE2_ROTATED, next);
567 return;
568 }
569 } catch (MSXException& /*e*/) {
570 // nothing
571 }
572 // Sector not found in 5 revolutions (or read error),
573 // schedule to give a RECORD_NOT_FOUND error
574 if (pulse5 < EmuTime::infinity()) {
575 schedule(FSM::TYPE2_NOT_FOUND, pulse5);
576 } else {
577 // Drive not rotating. How does a real WD293 handle this?
578 type2NotFound(time);
579 }
580}
581
582void WD2793::type2Rotated(EmuTime::param time)
583{
584 // The CRC status bit should only toggle after the disk has rotated
585 if (sectorInfo.addrCrcErr) {
586 statusReg |= CRC_ERROR;
587 } else {
588 statusReg &= ~CRC_ERROR;
589 }
590 if ((sectorInfo.addrCrcErr) ||
591 (sectorInfo.track != trackReg) ||
592 (sectorInfo.sector != sectorReg)) {
593 // TODO implement (optional) head compare
594 // not the sector we were looking for, continue searching
595 type2Search(time);
596 return;
597 }
598 if (sectorInfo.dataIdx == -1) {
599 // Sector header without accompanying data block.
600 // TODO we should actually wait for the disk to rotate before
601 // we can check this.
602 type2Search(time);
603 return;
604 }
605
606 // Ok, found matching sector.
607 switch (commandReg & 0xE0) {
608 case 0x80: // read sector or read sector multi
609 startReadSector(time);
610 break;
611
612 case 0xA0: // write sector or write sector multi
613 startWriteSector(time);
614 break;
615 }
616}
617
618void WD2793::type2NotFound(EmuTime::param time)
619{
620 statusReg |= RECORD_NOT_FOUND;
621 endCmd(time);
622}
623
624void WD2793::startReadSector(EmuTime::param time)
625{
626 if (sectorInfo.deleted) {
627 crc.init({0xA1, 0xA1, 0xA1, 0xF8});
628 } else {
629 crc.init({0xA1, 0xA1, 0xA1, 0xFB});
630 }
631 unsigned trackLength = drive.getTrackLength();
632 int tmp = sectorInfo.dataIdx - sectorInfo.addrIdx;
633 unsigned gapLength = (tmp >= 0) ? tmp : (tmp + trackLength);
634 assert(gapLength < trackLength);
635 drqTime.reset(time);
636 drqTime += gapLength + 1 + 1; // (first) byte can be read in a moment
637 dataCurrent = sectorInfo.dataIdx;
638
639 // Get sector size from disk: 128, 256, 512 or 1024 bytes
640 // Verified on real WD2793:
641 // size-code = 255 results in a sector size of 1024 bytes,
642 // This suggests the WD2793 only looks at the lower 2 bits.
643 dataAvailable = 128 << (sectorInfo.sizeCode & 3);
644}
645
646void WD2793::startWriteSector(EmuTime::param time)
647{
648 // At the current moment in time, the 'FE' byte in the address mark
649 // is located under the drive head (because the DMK format points to
650 // the 'FE' byte in the address header). After this byte there still
651 // follow the C,H,R,N and 2 crc bytes. So the address header ends in
652 // 7 bytes.
653 // - After 2 more bytes the WD2793 will activate DRQ.
654 // - 8 bytes later the WD2793 will check that the CPU has send the
655 // first byte (if not the command will be aborted without any writes
656 // to the disk, not even gap or data mark bytes).
657 // - after a pause of 12 bytes, the WD2793 will write 12 zero bytes,
658 // followed by the 4 bytes data header (A1 A1 A1 FB).
659 // - Next the WD2793 write the actual data bytes. At this moment it
660 // will also activate DRQ to receive the 2nd byte from the CPU.
661 //
662 // Note that between the 1st and 2nd activation of DRQ is a longer
663 // duration than between all later DRQ activations. The write-sector
664 // routine in Microsol_CDX-2 depends on this.
665
666 drqTime.reset(time);
667 drqTime += 7 + 2; // activate DRQ 2 bytes after end of address header
668
669 // 8 bytes later, the WD2793 will check whether the CPU wrote the
670 // first byte.
671 schedule(FSM::CHECK_WRITE, drqTime + 8);
672}
673
674void WD2793::checkStartWrite(EmuTime::param time)
675{
676 // By now the CPU should already have written the first byte, otherwise
677 // the write sector command doesn't even start.
678 if (!dataRegWritten) {
679 statusReg |= LOST_DATA;
680 endCmd(time);
681 return;
682 }
683
684 // From this point onwards the FDC will write a full sector to the disk
685 // (including markers and CRC). If the CPU doesn't supply data bytes in
686 // a timely manner, the missing bytes are filled with zero.
687 //
688 // But it is possible to cancel the write sector command to only write
689 // a partial sector (e.g. without CRC bytes). See cbsfox' comment in
690 // this forum thread:
691 // https://www.msx.org/forum/msx-talk/software/pdi-to-dmk-using-dsk-pro-104-with-openmsx
692 dataCurrent = sectorInfo.addrIdx
693 + 6 // C H R N CRC1 CRC2
694 + 22;
695
696 // pause 12 bytes
697 drqTime.reset(time);
698 schedule(FSM::PRE_WRITE_SECTOR, drqTime + 12);
699 drqTime.reset(EmuTime::infinity()); // DRQ = false
700 dataAvailable = 16; // 12+4 bytes pre-data
701}
702
703void WD2793::preWriteSector(EmuTime::param time)
704{
705 try {
706 --dataAvailable;
707 if (dataAvailable > 0) {
708 if (dataAvailable >= 4) {
709 // write 12 zero-bytes
710 drive.writeTrackByte(dataCurrent++, 0x00);
711 } else {
712 // followed by 3x A1-bytes
713 drive.writeTrackByte(dataCurrent++, 0xA1);
714 }
715 drqTime.reset(time);
716 schedule(FSM::PRE_WRITE_SECTOR, drqTime + 1);
717 drqTime.reset(EmuTime::infinity()); // DRQ = false
718 } else {
719 // and finally a single F8/FB byte
720 crc.init({0xA1, 0xA1, 0xA1});
721 uint8_t mark = (commandReg & A0_FLAG) ? 0xF8 : 0xFB;
722 drive.writeTrackByte(dataCurrent++, mark);
723 crc.update(mark);
724
725 // Pre-data is finished. Next start writing the actual data bytes
726 dataOutReg = dataReg;
727 dataRegWritten = false;
728 dataAvailable = 128 << (sectorInfo.sizeCode & 3); // see comment in startReadSector()
729
730 // Re-activate DRQ
731 drqTime.reset(time);
732
733 // Moment in time when first data byte gets written
734 schedule(FSM::WRITE_SECTOR, drqTime + 1);
735 }
736 } catch (MSXException&) {
737 statusReg |= NOT_READY; // TODO which status bit should be set?
738 endCmd(time);
739 }
740}
741
742void WD2793::writeSectorData(EmuTime::param time)
743{
744 try {
745 // Write data byte
746 drive.writeTrackByte(dataCurrent++, dataOutReg);
747 crc.update(dataOutReg);
748 --dataAvailable;
749
750 if (dataAvailable > 0) {
751 if (dataRegWritten) {
752 dataOutReg = dataReg;
753 dataRegWritten = false;
754 } else {
755 dataOutReg = 0;
756 statusReg |= LOST_DATA;
757 }
758 // Re-activate DRQ
759 drqTime.reset(time);
760
761 // Moment in time when next data byte gets written
762 schedule(FSM::WRITE_SECTOR, drqTime + 1);
763
764 if (dataAvailable == 1) {
765 // Don't reactivate DRQ if only the last
766 // data-byte still needs to be written to disk.
767 // At this point the CPU has already send that
768 // last byte.
769 drqTime.reset(EmuTime::infinity()); // DRQ = false
770 }
771 } else {
772 // Next write post-part
773 dataAvailable = 3;
774 drqTime.reset(time);
775 schedule(FSM::POST_WRITE_SECTOR, drqTime + 1);
776 drqTime.reset(EmuTime::infinity()); // DRQ = false
777 }
778 } catch (MSXException&) {
779 statusReg |= NOT_READY; // TODO which status bit should be set?
780 endCmd(time);
781 }
782}
783
784void WD2793::postWriteSector(EmuTime::param time)
785{
786 try {
787 --dataAvailable;
788 if (dataAvailable > 0) {
789 // write 2 CRC bytes (big endian)
790 uint8_t val = (dataAvailable == 2) ? narrow_cast<uint8_t>((crc.getValue() >> 8))
791 : narrow_cast<uint8_t>((crc.getValue() & 0xFF));
792 drive.writeTrackByte(dataCurrent++, val);
793 drqTime.reset(time);
794 schedule(FSM::POST_WRITE_SECTOR, drqTime + 1);
795 drqTime.reset(EmuTime::infinity()); // DRQ = false
796 } else {
797 // write one byte of 0xFE
798 drive.writeTrackByte(dataCurrent++, 0xFE);
799
800 // flush sector (actually full track) to disk.
801 drive.flushTrack();
802
803 if (!(commandReg & M_FLAG)) {
804 endCmd(time);
805 } else {
806 // multi sector write, wait for next sector
807 drqTime.reset(EmuTime::infinity()); // DRQ = false
808 sectorReg++;
809 type2Loaded(time);
810 }
811 }
812 } catch (MSXException&) {
813 // e.g. triggers when a different drive was selected during write
814 statusReg |= NOT_READY; // TODO which status bit should be set?
815 endCmd(time);
816 }
817}
818
819
820void WD2793::startType3Cmd(EmuTime::param time)
821{
822 statusReg &= ~(LOST_DATA | RECORD_NOT_FOUND | RECORD_TYPE);
823 statusReg |= BUSY;
824
825 if (!isReady()) {
826 endCmd(time);
827 } else {
828 if ((commandReg & 0xF0) == 0xF0) { // write track
829 // immediately activate DRQ
830 drqTime.reset(time); // DRQ = true
831 }
832
833 hldTime = time; // see comment in startType1Cmd
834 // WD2795/WD2797 would now set SSO output
835
836 if (commandReg & E_FLAG) {
837 schedule(FSM::TYPE3_LOADED,
838 time + EmuDuration::msec(30)); // when 1MHz clock
839 } else {
840 type3Loaded(time);
841 }
842 }
843}
844
845void WD2793::type3Loaded(EmuTime::param time)
846{
847 // TODO TG43 update
848 if (((commandReg & 0xF0) == 0xF0) && (drive.isWriteProtected())) {
849 // write track command and write protected
850 statusReg |= WRITE_PROTECTED;
851 endCmd(time);
852 return;
853 }
854
855 EmuTime next(EmuTime::dummy());
856 if ((commandReg & 0xF0) == 0xC0) {
857 // read address
858 try {
859 // wait till next sector header
860 setDrqRate(drive.getTrackLength());
861 next = drive.getNextSector(time, sectorInfo);
862 if (next == EmuTime::infinity()) {
863 // TODO wait for 5 revolutions
864 statusReg |= RECORD_NOT_FOUND;
865 endCmd(time);
866 return;
867 }
868 dataCurrent = sectorInfo.addrIdx;
869 dataAvailable = 6;
870 sectorReg = drive.readTrackByte(dataCurrent);
871 } catch (MSXException&) {
872 // read addr failed
873 statusReg |= RECORD_NOT_FOUND;
874 endCmd(time);
875 return;
876 }
877 } else {
878 // read/write track
879 // wait till next index pulse
880 next = drive.getTimeTillIndexPulse(time);
881 if (next == EmuTime::infinity()) {
882 // drive became not ready since the command was started,
883 // how does a real WD2793 handle this?
884 endCmd(time);
885 return;
886 }
887 }
888 schedule(FSM::TYPE3_ROTATED, next);
889}
890
891void WD2793::type3Rotated(EmuTime::param time)
892{
893 switch (commandReg & 0xF0) {
894 case 0xC0: // read Address
895 readAddressCmd(time);
896 break;
897 case 0xE0: // read track
898 readTrackCmd(time);
899 break;
900 case 0xF0: // write track
901 startWriteTrack(time);
902 break;
903 }
904}
905
906void WD2793::readAddressCmd(EmuTime::param time)
907{
908 drqTime.reset(time);
909 drqTime += 1; // (first) byte can be read in a moment
910}
911
912void WD2793::readTrackCmd(EmuTime::param time)
913{
914 try {
915 unsigned trackLength = drive.getTrackLength();
917 setDrqRate(trackLength);
918 dataCurrent = 0;
919 dataAvailable = narrow<int>(trackLength);
920 drqTime.reset(time);
921
922 // Stop command at next index pulse
923 schedule(FSM::READ_TRACK, drqTime + dataAvailable);
924
925 drqTime += 1; // (first) byte can be read in a moment
926 } catch (MSXException&) {
927 // read track failed, TODO status bits?
929 endCmd(time);
930 }
931}
932
933void WD2793::startWriteTrack(EmuTime::param time)
934{
935 // By now the CPU should already have written the first byte, otherwise
936 // the write track command doesn't even start.
937 if (!dataRegWritten) {
938 statusReg |= LOST_DATA;
939 endCmd(time);
940 return;
941 }
942 try {
943 unsigned trackLength = drive.getTrackLength();
944 setDrqRate(trackLength);
945 dataCurrent = 0;
946 dataAvailable = narrow<int>(trackLength);
947 lastWasA1 = false;
948 lastWasCRC = false;
949 dataOutReg = dataReg;
950 dataRegWritten = false;
951 drqTime.reset(time); // DRQ = true
952
953 // Moment in time when first track byte gets written
954 schedule(FSM::WRITE_TRACK, drqTime + 1);
955 } catch (MSXException& /*e*/) {
956 endCmd(time);
957 }
958}
959
960void WD2793::writeTrackData(EmuTime::param time)
961{
962 try {
963 bool prevA1 = lastWasA1;
964 lastWasA1 = false;
965
966 // handle chars with special meaning
967 uint8_t CRCvalue2 = 0; // dummy
968 bool idam = false;
969 if (lastWasCRC) {
970 // 2nd CRC byte, don't transform
971 lastWasCRC = false;
972 } else if (dataOutReg == 0xF5) {
973 // write A1 with missing clock transitions
974 dataOutReg = 0xA1;
975 lastWasA1 = true;
976 // Initialize CRC: the calculated CRC value
977 // includes the 3 A1 bytes. So when starting
978 // from the initial value 0xffff, we should not
979 // re-initialize the CRC value on the 2nd and
980 // 3rd A1 byte. Though what we do instead is on
981 // each A1 byte initialize the value as if
982 // there were already 2 A1 bytes written.
983 crc.init({0xA1, 0xA1});
984 } else if (dataOutReg == 0xF6) {
985 // write C2 with missing clock transitions
986 dataOutReg = 0xC2;
987 } else if (dataOutReg == 0xF7) {
988 // write 2 CRC bytes, big endian
989 dataOutReg = narrow_cast<uint8_t>(crc.getValue() >> 8); // high byte
990 CRCvalue2 = narrow_cast<uint8_t>(crc.getValue() >> 0); // low byte
991 lastWasCRC = true;
992 } else if (dataOutReg == 0xFE) {
993 // Record locations of 0xA1 (with missing clock
994 // transition) followed by 0xFE. The FE byte has
995 // no special meaning for the WD2793 itself,
996 // but it does for the DMK file format.
997 if (prevA1) idam = true;
998 }
999 // actually write (transformed) byte
1000 drive.writeTrackByte(dataCurrent++, dataOutReg, idam);
1001 crc.update(dataOutReg); // also when 'lastWasCRC == true'
1002 --dataAvailable;
1003
1004 if (dataAvailable > 0) {
1005 drqTime.reset(time); // DRQ = true
1006
1007 // Moment in time when next track byte gets written
1008 schedule(FSM::WRITE_TRACK, drqTime + 1);
1009
1010 // prepare next byte
1011 if (!lastWasCRC) {
1012 if (dataRegWritten) {
1013 dataOutReg = dataReg;
1014 dataRegWritten = false;
1015 } else {
1016 dataOutReg = 0;
1017 statusReg |= LOST_DATA;
1018 }
1019 } else {
1020 dataOutReg = CRCvalue2;
1021 // don't re-activate DRQ for 2nd byte of CRC
1022 drqTime.reset(EmuTime::infinity()); // DRQ = false
1023 }
1024 } else {
1025 // Write track done
1026 drive.flushTrack();
1027 endCmd(time);
1028 }
1029 } catch (MSXException&) {
1030 statusReg |= NOT_READY; // TODO which status bit should be set?
1031 endCmd(time);
1032 }
1033}
1034
1035void WD2793::startType4Cmd(EmuTime::param time)
1036{
1037 // Force interrupt
1038 uint8_t flags = commandReg & 0x0F;
1039 if (flags & (N2R_IRQ | R2N_IRQ)) {
1040 // all flags not yet supported
1041 #ifdef DEBUG
1042 std::cerr << "WD2793 type 4 cmd, unimplemented bits " << int(flags) << '\n';
1043 #endif
1044 }
1045
1046 if (flags == 0x00) {
1047 immediateIRQ = false;
1048 }
1049 if ((flags & IDX_IRQ) && isReady()) {
1050 irqTime = drive.getTimeTillIndexPulse(time);
1051 } else {
1052 assert(irqTime == EmuTime::infinity()); // INTRQ = false
1053 }
1054 if (flags & IMM_IRQ) {
1055 immediateIRQ = true;
1056 }
1057
1058 drqTime.reset(EmuTime::infinity()); // DRQ = false
1059 statusReg &= ~BUSY; // reset status on Busy
1060}
1061
1062void WD2793::endCmd(EmuTime::param time)
1063{
1064 if ((hldTime <= time) && (time < (hldTime + IDLE))) {
1065 // HLD was active, start timeout period
1066 // Real WD2793 waits for 15 index pulses. We approximate that
1067 // here by waiting for 3s.
1068 hldTime = time;
1069 }
1070 drqTime.reset(EmuTime::infinity()); // DRQ = false
1071 irqTime = EmuTime::zero(); // INTRQ = true;
1072 statusReg &= ~BUSY;
1073}
1074
1075
1076static constexpr std::initializer_list<enum_string<WD2793::FSM>> fsmStateInfo = {
1077 { "NONE", WD2793::FSM::NONE },
1078 { "SEEK", WD2793::FSM::SEEK },
1079 { "TYPE2_LOADED", WD2793::FSM::TYPE2_LOADED },
1080 { "TYPE2_NOT_FOUND", WD2793::FSM::TYPE2_NOT_FOUND },
1081 { "TYPE2_ROTATED", WD2793::FSM::TYPE2_ROTATED },
1082 { "CHECK_WRITE", WD2793::FSM::CHECK_WRITE },
1083 { "PRE_WRITE_SECTOR", WD2793::FSM::PRE_WRITE_SECTOR },
1084 { "WRITE_SECTOR", WD2793::FSM::WRITE_SECTOR },
1085 { "POST_WRITE_SECTOR", WD2793::FSM::POST_WRITE_SECTOR },
1086 { "TYPE3_LOADED", WD2793::FSM::TYPE3_LOADED },
1087 { "TYPE3_ROTATED", WD2793::FSM::TYPE3_ROTATED },
1088 { "WRITE_TRACK", WD2793::FSM::WRITE_TRACK },
1089 { "READ_TRACK", WD2793::FSM::READ_TRACK },
1090 { "IDX_IRQ", WD2793::FSM::IDX_IRQ },
1091 // for bw-compat savestate
1092 { "TYPE2_WAIT_LOAD", WD2793::FSM::TYPE2_LOADED }, // was FSM::TYPE2_WAIT_LOAD
1093 { "TYPE3_WAIT_LOAD", WD2793::FSM::TYPE3_LOADED }, // was FSM::TYPE3_WAIT_LOAD
1094};
1096
1097// version 1: initial version
1098// version 2: removed members: commandStart, DRQTimer, DRQ, transferring, formatting
1099// added member: drqTime (has different semantics than DRQTimer)
1100// also the timing of the data-transfer commands (read/write sector
1101// and write track) has changed. So this could result in replay-sync
1102// errors.
1103// (Also the enum FSMState has changed, but that's not a problem.)
1104// version 3: Added members 'crc' and 'lastWasA1'.
1105// Replaced 'dataBuffer' with 'trackData'. We don't attempt to migrate
1106// the old 'dataBuffer' content to 'trackData' (doing so would be
1107// quite difficult). This means that old savestates that were in the
1108// middle of a sector/track read/write command probably won't work
1109// correctly anymore. We do give a warning on this.
1110// version 4: changed type of drqTime from Clock to DynamicClock
1111// version 5: added 'pulse5' and 'sectorInfo'
1112// version 6: no layout changes, only added new enum value 'FSM::CHECK_WRITE'
1113// version 7: replaced 'bool INTRQ' with 'EmuTime irqTime'
1114// version 8: removed 'userData' from Schedulable
1115// version 9: added 'trackDataValid'
1116// version 10: removed 'trackData' and 'trackDataValid' (moved to RealDrive)
1117// version 11: added 'dataOutReg', 'dataRegWritten', 'lastWasCRC'
1118// version 12: added 'hldTime'
1119template<typename Archive>
1120void WD2793::serialize(Archive& ar, unsigned version)
1121{
1122 EmuTime bw_irqTime = EmuTime::zero();
1123 if (ar.versionAtLeast(version, 8)) {
1124 ar.template serializeBase<Schedulable>(*this);
1125 } else {
1126 constexpr int SCHED_FSM = 0;
1127 constexpr int SCHED_IDX_IRQ = 1;
1128 assert(Archive::IS_LOADER);
1130 for (auto& old : Schedulable::serializeBW(ar)) {
1131 if (old.userData == SCHED_FSM) {
1132 setSyncPoint(old.time);
1133 } else if (old.userData == SCHED_IDX_IRQ) {
1134 bw_irqTime = old.time;
1135 }
1136 }
1137 }
1138
1139 ar.serialize("fsmState", fsmState,
1140 "statusReg", statusReg,
1141 "commandReg", commandReg,
1142 "sectorReg", sectorReg,
1143 "trackReg", trackReg,
1144 "dataReg", dataReg,
1145
1146 "directionIn", directionIn,
1147 "immediateIRQ", immediateIRQ,
1148
1149 "dataCurrent", dataCurrent,
1150 "dataAvailable", dataAvailable);
1151
1152 if (ar.versionAtLeast(version, 2)) {
1153 if (ar.versionAtLeast(version, 4)) {
1154 ar.serialize("drqTime", drqTime);
1155 } else {
1156 assert(Archive::IS_LOADER);
1157 Clock<6250 * 5> c(EmuTime::dummy());
1158 ar.serialize("drqTime", c);
1159 drqTime.reset(c.getTime());
1160 drqTime.setFreq(6250 * 5);
1161 }
1162 } else {
1163 assert(Archive::IS_LOADER);
1164 //ar.serialize("commandStart", commandStart,
1165 // "DRQTimer", DRQTimer,
1166 // "DRQ", DRQ,
1167 // "transferring", transferring,
1168 // "formatting", formatting);
1169 drqTime.reset(EmuTime::infinity());
1170 }
1171
1172 if (ar.versionAtLeast(version, 3)) {
1173 ar.serialize("lastWasA1", lastWasA1);
1174 uint16_t crcVal = crc.getValue();
1175 ar.serialize("crc", crcVal);
1176 crc.init(crcVal);
1177 }
1178
1179 if (ar.versionAtLeast(version, 5)) {
1180 ar.serialize("pulse5", pulse5,
1181 "sectorInfo", sectorInfo);
1182 } else {
1183 // leave pulse5 at EmuTime::infinity()
1184 // leave sectorInfo uninitialized
1185 }
1186
1187 if (ar.versionAtLeast(version, 7)) {
1188 ar.serialize("irqTime", irqTime);
1189 } else {
1190 assert(Archive::IS_LOADER);
1191 bool INTRQ = false; // dummy init to avoid warning
1192 ar.serialize("INTRQ", INTRQ);
1193 irqTime = INTRQ ? EmuTime::zero() : EmuTime::infinity();
1194 if (bw_irqTime != EmuTime::zero()) {
1195 irqTime = bw_irqTime;
1196 }
1197 }
1198
1199 if (ar.versionAtLeast(version, 11)) {
1200 ar.serialize("dataOutReg", dataOutReg,
1201 "dataRegWritten", dataRegWritten,
1202 "lastWasCRC", lastWasCRC);
1203 } else {
1204 assert(Archive::IS_LOADER);
1205 dataOutReg = dataReg;
1206 dataRegWritten = false;
1207 lastWasCRC = false;
1208 }
1209
1210 if (ar.versionBelow(version, 11)) {
1211 assert(Archive::IS_LOADER);
1212 // version 9->10: 'trackData' moved from FDC to RealDrive
1213 // version 10->11: write commands are different
1214 if (statusReg & BUSY) {
1215 cliComm.printWarning(
1216 "Loading an old savestate that has an "
1217 "in-progress WD2793 command. This is not "
1218 "fully backwards-compatible and can cause "
1219 "wrong emulation behavior.");
1220 }
1221 }
1222
1223 if (ar.versionAtLeast(version, 12)) {
1224 ar.serialize("hldTime", hldTime);
1225 } else {
1226 if (statusReg & BUSY) {
1227 hldTime = getCurrentTime();
1228 } else {
1229 hldTime = EmuTime::infinity();
1230 }
1231 }
1232}
1234
1235} // 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:12
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
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:1120
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:11
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