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