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