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