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 } else {
764 // Next write post-part
765 dataAvailable = 3;
766 drqTime.reset(time);
767 schedule(FSM::POST_WRITE_SECTOR, drqTime + 1);
768 drqTime.reset(EmuTime::infinity()); // DRQ = false
769 }
770 } catch (MSXException&) {
771 statusReg |= NOT_READY; // TODO which status bit should be set?
772 endCmd(time);
773 }
774}
775
776void WD2793::postWriteSector(EmuTime::param time)
777{
778 try {
779 --dataAvailable;
780 if (dataAvailable > 0) {
781 // write 2 CRC bytes (big endian)
782 uint8_t val = (dataAvailable == 2) ? narrow_cast<uint8_t>((crc.getValue() >> 8))
783 : narrow_cast<uint8_t>((crc.getValue() & 0xFF));
784 drive.writeTrackByte(dataCurrent++, val);
785 drqTime.reset(time);
786 schedule(FSM::POST_WRITE_SECTOR, drqTime + 1);
787 drqTime.reset(EmuTime::infinity()); // DRQ = false
788 } else {
789 // write one byte of 0xFE
790 drive.writeTrackByte(dataCurrent++, 0xFE);
791
792 // flush sector (actually full track) to disk.
793 drive.flushTrack();
794
795 if (!(commandReg & M_FLAG)) {
796 endCmd(time);
797 } else {
798 // multi sector write, wait for next sector
799 drqTime.reset(EmuTime::infinity()); // DRQ = false
800 sectorReg++;
801 type2Loaded(time);
802 }
803 }
804 } catch (MSXException&) {
805 // e.g. triggers when a different drive was selected during write
806 statusReg |= NOT_READY; // TODO which status bit should be set?
807 endCmd(time);
808 }
809}
810
811
812void WD2793::startType3Cmd(EmuTime::param time)
813{
814 statusReg &= ~(LOST_DATA | RECORD_NOT_FOUND | RECORD_TYPE);
815 statusReg |= BUSY;
816
817 if (!isReady()) {
818 endCmd(time);
819 } else {
820 if ((commandReg & 0xF0) == 0xF0) { // write track
821 // immediately activate DRQ
822 drqTime.reset(time); // DRQ = true
823 }
824
825 hldTime = time; // see comment in startType1Cmd
826 // WD2795/WD2797 would now set SSO output
827
828 if (commandReg & E_FLAG) {
829 schedule(FSM::TYPE3_LOADED,
830 time + EmuDuration::msec(30)); // when 1MHz clock
831 } else {
832 type3Loaded(time);
833 }
834 }
835}
836
837void WD2793::type3Loaded(EmuTime::param time)
838{
839 // TODO TG43 update
840 if (((commandReg & 0xF0) == 0xF0) && (drive.isWriteProtected())) {
841 // write track command and write protected
842 statusReg |= WRITE_PROTECTED;
843 endCmd(time);
844 return;
845 }
846
847 EmuTime next(EmuTime::dummy());
848 if ((commandReg & 0xF0) == 0xC0) {
849 // read address
850 try {
851 // wait till next sector header
852 setDrqRate(drive.getTrackLength());
853 next = drive.getNextSector(time, sectorInfo);
854 if (next == EmuTime::infinity()) {
855 // TODO wait for 5 revolutions
856 statusReg |= RECORD_NOT_FOUND;
857 endCmd(time);
858 return;
859 }
860 dataCurrent = sectorInfo.addrIdx;
861 dataAvailable = 6;
862 sectorReg = drive.readTrackByte(dataCurrent);
863 } catch (MSXException&) {
864 // read addr failed
865 statusReg |= RECORD_NOT_FOUND;
866 endCmd(time);
867 return;
868 }
869 } else {
870 // read/write track
871 // wait till next index pulse
872 next = drive.getTimeTillIndexPulse(time);
873 if (next == EmuTime::infinity()) {
874 // drive became not ready since the command was started,
875 // how does a real WD2793 handle this?
876 endCmd(time);
877 return;
878 }
879 }
880 schedule(FSM::TYPE3_ROTATED, next);
881}
882
883void WD2793::type3Rotated(EmuTime::param time)
884{
885 switch (commandReg & 0xF0) {
886 case 0xC0: // read Address
887 readAddressCmd(time);
888 break;
889 case 0xE0: // read track
890 readTrackCmd(time);
891 break;
892 case 0xF0: // write track
893 startWriteTrack(time);
894 break;
895 }
896}
897
898void WD2793::readAddressCmd(EmuTime::param time)
899{
900 drqTime.reset(time);
901 drqTime += 1; // (first) byte can be read in a moment
902}
903
904void WD2793::readTrackCmd(EmuTime::param time)
905{
906 try {
907 unsigned trackLength = drive.getTrackLength();
909 setDrqRate(trackLength);
910 dataCurrent = 0;
911 dataAvailable = narrow<int>(trackLength);
912 drqTime.reset(time);
913
914 // Stop command at next index pulse
915 schedule(FSM::READ_TRACK, drqTime + dataAvailable);
916
917 drqTime += 1; // (first) byte can be read in a moment
918 } catch (MSXException&) {
919 // read track failed, TODO status bits?
921 endCmd(time);
922 }
923}
924
925void WD2793::startWriteTrack(EmuTime::param time)
926{
927 // By now the CPU should already have written the first byte, otherwise
928 // the write track command doesn't even start.
929 if (!dataRegWritten) {
930 statusReg |= LOST_DATA;
931 endCmd(time);
932 return;
933 }
934 try {
935 unsigned trackLength = drive.getTrackLength();
936 setDrqRate(trackLength);
937 dataCurrent = 0;
938 dataAvailable = narrow<int>(trackLength);
939 lastWasA1 = false;
940 lastWasCRC = false;
941 dataOutReg = dataReg;
942 dataRegWritten = false;
943 drqTime.reset(time); // DRQ = true
944
945 // Moment in time when first track byte gets written
946 schedule(FSM::WRITE_TRACK, drqTime + 1);
947 } catch (MSXException& /*e*/) {
948 endCmd(time);
949 }
950}
951
952void WD2793::writeTrackData(EmuTime::param time)
953{
954 try {
955 bool prevA1 = lastWasA1;
956 lastWasA1 = false;
957
958 // handle chars with special meaning
959 uint8_t CRCvalue2 = 0; // dummy
960 bool idam = false;
961 if (lastWasCRC) {
962 // 2nd CRC byte, don't transform
963 lastWasCRC = false;
964 } else if (dataOutReg == 0xF5) {
965 // write A1 with missing clock transitions
966 dataOutReg = 0xA1;
967 lastWasA1 = true;
968 // Initialize CRC: the calculated CRC value
969 // includes the 3 A1 bytes. So when starting
970 // from the initial value 0xffff, we should not
971 // re-initialize the CRC value on the 2nd and
972 // 3rd A1 byte. Though what we do instead is on
973 // each A1 byte initialize the value as if
974 // there were already 2 A1 bytes written.
975 crc.init({0xA1, 0xA1});
976 } else if (dataOutReg == 0xF6) {
977 // write C2 with missing clock transitions
978 dataOutReg = 0xC2;
979 } else if (dataOutReg == 0xF7) {
980 // write 2 CRC bytes, big endian
981 dataOutReg = narrow_cast<uint8_t>(crc.getValue() >> 8); // high byte
982 CRCvalue2 = narrow_cast<uint8_t>(crc.getValue() >> 0); // low byte
983 lastWasCRC = true;
984 } else if (dataOutReg == 0xFE) {
985 // Record locations of 0xA1 (with missing clock
986 // transition) followed by 0xFE. The FE byte has
987 // no special meaning for the WD2793 itself,
988 // but it does for the DMK file format.
989 if (prevA1) idam = true;
990 }
991 // actually write (transformed) byte
992 drive.writeTrackByte(dataCurrent++, dataOutReg, idam);
993 crc.update(dataOutReg); // also when 'lastWasCRC == true'
994 --dataAvailable;
995
996 if (dataAvailable > 0) {
997 drqTime.reset(time); // DRQ = true
998
999 // Moment in time when next track byte gets written
1000 schedule(FSM::WRITE_TRACK, drqTime + 1);
1001
1002 // prepare next byte
1003 if (!lastWasCRC) {
1004 if (dataRegWritten) {
1005 dataOutReg = dataReg;
1006 dataRegWritten = false;
1007 } else {
1008 dataOutReg = 0;
1009 statusReg |= LOST_DATA;
1010 }
1011 } else {
1012 dataOutReg = CRCvalue2;
1013 // don't re-activate DRQ for 2nd byte of CRC
1014 drqTime.reset(EmuTime::infinity()); // DRQ = false
1015 }
1016 } else {
1017 // Write track done
1018 drive.flushTrack();
1019 endCmd(time);
1020 }
1021 } catch (MSXException&) {
1022 statusReg |= NOT_READY; // TODO which status bit should be set?
1023 endCmd(time);
1024 }
1025}
1026
1027void WD2793::startType4Cmd(EmuTime::param time)
1028{
1029 // Force interrupt
1030 uint8_t flags = commandReg & 0x0F;
1031 if (flags & (N2R_IRQ | R2N_IRQ)) {
1032 // all flags not yet supported
1033 #ifdef DEBUG
1034 std::cerr << "WD2793 type 4 cmd, unimplemented bits " << int(flags) << '\n';
1035 #endif
1036 }
1037
1038 if (flags == 0x00) {
1039 immediateIRQ = false;
1040 }
1041 if ((flags & IDX_IRQ) && isReady()) {
1042 irqTime = drive.getTimeTillIndexPulse(time);
1043 } else {
1044 assert(irqTime == EmuTime::infinity()); // INTRQ = false
1045 }
1046 if (flags & IMM_IRQ) {
1047 immediateIRQ = true;
1048 }
1049
1050 drqTime.reset(EmuTime::infinity()); // DRQ = false
1051 statusReg &= ~BUSY; // reset status on Busy
1052}
1053
1054void WD2793::endCmd(EmuTime::param time)
1055{
1056 if ((hldTime <= time) && (time < (hldTime + IDLE))) {
1057 // HLD was active, start timeout period
1058 // Real WD2793 waits for 15 index pulses. We approximate that
1059 // here by waiting for 3s.
1060 hldTime = time;
1061 }
1062 drqTime.reset(EmuTime::infinity()); // DRQ = false
1063 irqTime = EmuTime::zero(); // INTRQ = true;
1064 statusReg &= ~BUSY;
1065}
1066
1067
1068static constexpr std::initializer_list<enum_string<WD2793::FSM>> fsmStateInfo = {
1069 { "NONE", WD2793::FSM::NONE },
1070 { "SEEK", WD2793::FSM::SEEK },
1071 { "TYPE2_LOADED", WD2793::FSM::TYPE2_LOADED },
1072 { "TYPE2_NOT_FOUND", WD2793::FSM::TYPE2_NOT_FOUND },
1073 { "TYPE2_ROTATED", WD2793::FSM::TYPE2_ROTATED },
1074 { "CHECK_WRITE", WD2793::FSM::CHECK_WRITE },
1075 { "PRE_WRITE_SECTOR", WD2793::FSM::PRE_WRITE_SECTOR },
1076 { "WRITE_SECTOR", WD2793::FSM::WRITE_SECTOR },
1077 { "POST_WRITE_SECTOR", WD2793::FSM::POST_WRITE_SECTOR },
1078 { "TYPE3_LOADED", WD2793::FSM::TYPE3_LOADED },
1079 { "TYPE3_ROTATED", WD2793::FSM::TYPE3_ROTATED },
1080 { "WRITE_TRACK", WD2793::FSM::WRITE_TRACK },
1081 { "READ_TRACK", WD2793::FSM::READ_TRACK },
1082 { "IDX_IRQ", WD2793::FSM::IDX_IRQ },
1083 // for bw-compat savestate
1084 { "TYPE2_WAIT_LOAD", WD2793::FSM::TYPE2_LOADED }, // was FSM::TYPE2_WAIT_LOAD
1085 { "TYPE3_WAIT_LOAD", WD2793::FSM::TYPE3_LOADED }, // was FSM::TYPE3_WAIT_LOAD
1086};
1088
1089// version 1: initial version
1090// version 2: removed members: commandStart, DRQTimer, DRQ, transferring, formatting
1091// added member: drqTime (has different semantics than DRQTimer)
1092// also the timing of the data-transfer commands (read/write sector
1093// and write track) has changed. So this could result in replay-sync
1094// errors.
1095// (Also the enum FSMState has changed, but that's not a problem.)
1096// version 3: Added members 'crc' and 'lastWasA1'.
1097// Replaced 'dataBuffer' with 'trackData'. We don't attempt to migrate
1098// the old 'dataBuffer' content to 'trackData' (doing so would be
1099// quite difficult). This means that old savestates that were in the
1100// middle of a sector/track read/write command probably won't work
1101// correctly anymore. We do give a warning on this.
1102// version 4: changed type of drqTime from Clock to DynamicClock
1103// version 5: added 'pulse5' and 'sectorInfo'
1104// version 6: no layout changes, only added new enum value 'FSM::CHECK_WRITE'
1105// version 7: replaced 'bool INTRQ' with 'EmuTime irqTime'
1106// version 8: removed 'userData' from Schedulable
1107// version 9: added 'trackDataValid'
1108// version 10: removed 'trackData' and 'trackDataValid' (moved to RealDrive)
1109// version 11: added 'dataOutReg', 'dataRegWritten', 'lastWasCRC'
1110// version 12: added 'hldTime'
1111template<typename Archive>
1112void WD2793::serialize(Archive& ar, unsigned version)
1113{
1114 EmuTime bw_irqTime = EmuTime::zero();
1115 if (ar.versionAtLeast(version, 8)) {
1116 ar.template serializeBase<Schedulable>(*this);
1117 } else {
1118 constexpr int SCHED_FSM = 0;
1119 constexpr int SCHED_IDX_IRQ = 1;
1120 assert(Archive::IS_LOADER);
1122 for (auto& old : Schedulable::serializeBW(ar)) {
1123 if (old.userData == SCHED_FSM) {
1124 setSyncPoint(old.time);
1125 } else if (old.userData == SCHED_IDX_IRQ) {
1126 bw_irqTime = old.time;
1127 }
1128 }
1129 }
1130
1131 ar.serialize("fsmState", fsmState,
1132 "statusReg", statusReg,
1133 "commandReg", commandReg,
1134 "sectorReg", sectorReg,
1135 "trackReg", trackReg,
1136 "dataReg", dataReg,
1137
1138 "directionIn", directionIn,
1139 "immediateIRQ", immediateIRQ,
1140
1141 "dataCurrent", dataCurrent,
1142 "dataAvailable", dataAvailable);
1143
1144 if (ar.versionAtLeast(version, 2)) {
1145 if (ar.versionAtLeast(version, 4)) {
1146 ar.serialize("drqTime", drqTime);
1147 } else {
1148 assert(Archive::IS_LOADER);
1149 Clock<6250 * 5> c(EmuTime::dummy());
1150 ar.serialize("drqTime", c);
1151 drqTime.reset(c.getTime());
1152 drqTime.setFreq(6250 * 5);
1153 }
1154 } else {
1155 assert(Archive::IS_LOADER);
1156 //ar.serialize("commandStart", commandStart,
1157 // "DRQTimer", DRQTimer,
1158 // "DRQ", DRQ,
1159 // "transferring", transferring,
1160 // "formatting", formatting);
1161 drqTime.reset(EmuTime::infinity());
1162 }
1163
1164 if (ar.versionAtLeast(version, 3)) {
1165 ar.serialize("lastWasA1", lastWasA1);
1166 uint16_t crcVal = crc.getValue();
1167 ar.serialize("crc", crcVal);
1168 crc.init(crcVal);
1169 }
1170
1171 if (ar.versionAtLeast(version, 5)) {
1172 ar.serialize("pulse5", pulse5,
1173 "sectorInfo", sectorInfo);
1174 } else {
1175 // leave pulse5 at EmuTime::infinity()
1176 // leave sectorInfo uninitialized
1177 }
1178
1179 if (ar.versionAtLeast(version, 7)) {
1180 ar.serialize("irqTime", irqTime);
1181 } else {
1182 assert(Archive::IS_LOADER);
1183 bool INTRQ = false; // dummy init to avoid warning
1184 ar.serialize("INTRQ", INTRQ);
1185 irqTime = INTRQ ? EmuTime::zero() : EmuTime::infinity();
1186 if (bw_irqTime != EmuTime::zero()) {
1187 irqTime = bw_irqTime;
1188 }
1189 }
1190
1191 if (ar.versionAtLeast(version, 11)) {
1192 ar.serialize("dataOutReg", dataOutReg,
1193 "dataRegWritten", dataRegWritten,
1194 "lastWasCRC", lastWasCRC);
1195 } else {
1196 assert(Archive::IS_LOADER);
1197 dataOutReg = dataReg;
1198 dataRegWritten = false;
1199 lastWasCRC = false;
1200 }
1201
1202 if (ar.versionBelow(version, 11)) {
1203 assert(Archive::IS_LOADER);
1204 // version 9->10: 'trackData' moved from FDC to RealDrive
1205 // version 10->11: write commands are different
1206 if (statusReg & BUSY) {
1207 cliComm.printWarning(
1208 "Loading an old savestate that has an "
1209 "in-progress WD2793 command. This is not "
1210 "fully backwards-compatible and can cause "
1211 "wrong emulation behavior.");
1212 }
1213 }
1214
1215 if (ar.versionAtLeast(version, 12)) {
1216 ar.serialize("hldTime", hldTime);
1217 } else {
1218 if (statusReg & BUSY) {
1219 hldTime = getCurrentTime();
1220 } else {
1221 hldTime = EmuTime::infinity();
1222 }
1223 }
1224}
1226
1227} // 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
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:1112
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