openMSX
RealDrive.cc
Go to the documentation of this file.
1 #include "RealDrive.hh"
2 #include "Disk.hh"
3 #include "MSXMotherBoard.hh"
4 #include "Reactor.hh"
5 #include "LedStatus.hh"
7 #include "CliComm.hh"
8 #include "GlobalSettings.hh"
9 #include "MSXException.hh"
10 #include "serialize.hh"
11 #include "unreachable.hh"
12 #include <memory>
13 
14 namespace openmsx {
15 
17  bool signalsNeedMotorOn_, bool doubleSided,
18  DiskDrive::TrackMode trackMode_)
19  : syncLoadingTimeout(motherBoard_.getScheduler())
20  , syncMotorTimeout (motherBoard_.getScheduler())
21  , motherBoard(motherBoard_)
22  , loadingIndicator(
23  motherBoard.getReactor().getGlobalSettings().getThrottleManager())
24  , motorTimeout(motorTimeout_)
25  , motorTimer(getCurrentTime())
26  , headPos(0), side(0), startAngle(0)
27  , motorStatus(false)
28  , doubleSizedDrive(doubleSided)
29  , signalsNeedMotorOn(signalsNeedMotorOn_)
30  , trackMode(trackMode_)
31  , trackValid(false), trackDirty(false)
32 {
33  drivesInUse = motherBoard.getSharedStuff<DrivesInUse>("drivesInUse");
34 
35  unsigned i = 0;
36  while ((*drivesInUse)[i]) {
37  if (++i == MAX_DRIVES) {
38  throw MSXException("Too many disk drives.");
39  }
40  }
41  (*drivesInUse)[i] = true;
42  std::string driveName = "diskX"; driveName[4] = char('a' + i);
43 
44  if (motherBoard.getMSXCommandController().hasCommand(driveName)) {
45  throw MSXException("Duplicated drive name: ", driveName);
46  }
47  motherBoard.getMSXCliComm().update(CliComm::HARDWARE, driveName, "add");
48  changer.emplace(motherBoard, driveName, true, doubleSizedDrive,
49  [this]() { invalidateTrack(); });
50 }
51 
53 {
54  try {
55  flushTrack();
56  } catch (MSXException&) {
57  // ignore
58  }
59  doSetMotor(false, getCurrentTime()); // to send LED event
60 
61  const auto& driveName = changer->getDriveName();
62  motherBoard.getMSXCliComm().update(CliComm::HARDWARE, driveName, "remove");
63 
64  unsigned driveNum = driveName[4] - 'a';
65  assert((*drivesInUse)[driveNum]);
66  (*drivesInUse)[driveNum] = false;
67 }
68 
70 {
71  // The game 'Trojka' mentions on the disk label that it works on a
72  // single-sided drive. The 2nd side of the disk actually contains a
73  // copy protection (obviously only checked on machines with a double
74  // sided drive). This copy-protection works fine in openMSX (when using
75  // a proper DMK disk image). The real disk also runs fine on machines
76  // with single sided drives. Though when we initially ran this game in
77  // an emulated machine with a single sided drive, the copy-protection
78  // check didn't pass. Initially we emulated single sided drives by
79  // simply ignoring the side-select signal. Now when the 2nd side is
80  // selected on a single sided drive, we disable the drive-ready signal.
81  // This makes the 'Trojka' copy-protection check pass.
82  // TODO verify that this is indeed how single sided drives behave
83  if (!doubleSizedDrive && (side != 0)) return false;
84 
85  return !changer->getDisk().isDummyDisk();
86 }
87 
89 {
90  // On a NMS8280 the write protected signal is never active when the
91  // drive motor is turned off. See also isTrack00().
92  if (signalsNeedMotorOn && !motorStatus) return false;
93  return changer->getDisk().isWriteProtected();
94 }
95 
97 {
98  return doubleSizedDrive ? changer->getDisk().isDoubleSided()
99  : false;
100 }
101 
102 void RealDrive::setSide(bool side_)
103 {
104  invalidateTrack();
105  side = side_ ? 1 : 0; // also for single-sided drives
106 }
107 
108 bool RealDrive::getSide() const
109 {
110  return side;
111 }
112 
113 unsigned RealDrive::getMaxTrack() const
114 {
115  constexpr unsigned MAX_TRACK = 85;
116  switch (trackMode) {
118  return MAX_TRACK;
120  // Yamaha FD-03: Tracks are calibrated around 4 steps per track, max 3 step further than base
121  return MAX_TRACK * 4 + 3;
122  default:
123  UNREACHABLE;
124  return 0;
125  }
126 }
127 
128 std::optional<unsigned> RealDrive::getDiskReadTrack() const
129 {
130  // Translate head-position to track number on disk.
131  // Normally this is a 1-to-1 mapping, but on the Yamaha-FD-03 the head
132  // moves 4 steps for every track on disk. This means it can be
133  // between disk tracks so that it can't read valid data.
134  switch (trackMode) {
136  return headPos;
138  // Tracks are at a multiple of 4 steps.
139  // But also make sure the track at 1 step lower and 1 step up correctly reads.
140  // This makes the driver (calibration routine) use a trackoffset of 0 (so at a multiple of 4 steps).
141  if ((headPos >= 2) && ((headPos % 4) != 2)) {
142  return (headPos - 2) / 4;
143  } else {
144  return {};
145  }
146  default:
147  UNREACHABLE;
148  return {};
149  }
150 }
151 
152 std::optional<unsigned> RealDrive::getDiskWriteTrack() const
153 {
154  switch (trackMode) {
156  return headPos;
158  // For writes allow only exact multiples of 4. But track 0 is at step 4
159  if ((headPos >= 4) && ((headPos % 4) == 0)) {
160  return (headPos - 4) / 4;
161  } else {
162  return {};
163  }
164  default:
165  UNREACHABLE;
166  return {};
167  }
168 }
169 
170 void RealDrive::step(bool direction, EmuTime::param time)
171 {
172  invalidateTrack();
173 
174  if (direction) {
175  // step in
176  if (headPos < getMaxTrack()) {
177  headPos++;
178  }
179  } else {
180  // step out
181  if (headPos > 0) {
182  headPos--;
183  }
184  }
185  // ThrottleManager heuristic:
186  // If the motor is turning and there is head movement, assume the
187  // MSX program is (still) loading/saving to disk
188  if (motorStatus) setLoading(time);
189 }
190 
192 {
193  // On a Philips-NMS8280 the track00 signal is never active when the
194  // drive motor is turned off. On a National-FS-5500F2 the motor status
195  // doesn't matter, the single/dual drive detection routine (during
196  // the MSX boot sequence) even depends on this signal with motor off.
197  if (signalsNeedMotorOn && !motorStatus) return false;
198  return headPos == 0;
199 }
200 
201 void RealDrive::setMotor(bool status, EmuTime::param time)
202 {
203  // If status = true, motor is immediately turned on. If status = false,
204  // the motor is only turned off after some (configurable) amount of
205  // time (can be zero). Let's call the last passed status parameter the
206  // 'logical' motor status.
207  //
208  // Loading indicator heuristic:
209  // Loading indicator only reacts to _changes_ in the _logical_ motor
210  // status. So when the motor is turned off, we immediately assume the
211  // MSX program is done loading (or saving) (we don't wait for the motor
212  // timeout). Turning the motor on when it already was logically on has
213  // no effect. But turning it back on while it was logically off but
214  // still in the motor-off-timeout phase does reset the loading
215  // indicator.
216  //
217  if (status) {
218  // (Try to) remove scheduled action to turn motor off.
219  if (syncMotorTimeout.removeSyncPoint()) {
220  // If there actually was such an action scheduled, we
221  // need to turn on the loading indicator.
222  assert(motorStatus);
223  setLoading(time);
224  return;
225  }
226  if (motorStatus) {
227  // Motor was still turning, we're done.
228  // Note: no effect on loading indicator.
229  return;
230  }
231  // Actually turn motor on (it was off before).
232  doSetMotor(true, time);
233  setLoading(time);
234  } else {
235  if (!motorStatus) {
236  // Motor was already off, we're done.
237  return;
238  }
239  if (syncMotorTimeout.pendingSyncPoint()) {
240  // We had already scheduled an action to turn the motor
241  // off, we're done.
242  return;
243  }
244  // Heuristic:
245  // Immediately react to 'logical' motor status, even if the
246  // motor will (possibly) still keep rotating for a few
247  // seconds.
248  syncLoadingTimeout.removeSyncPoint();
249  loadingIndicator.update(false);
250 
251  // Turn the motor off after some timeout (timeout could be 0)
252  syncMotorTimeout.setSyncPoint(time + motorTimeout);
253  }
254 }
255 
257 {
258  // note: currently unused because of the implementation in DriveMultiplexer
259  // note: currently returns the actual motor status, could be different from the
260  // last set status because of 'syncMotorTimeout'.
261  return motorStatus;
262 }
263 
264 unsigned RealDrive::getCurrentAngle(EmuTime::param time) const
265 {
266  if (motorStatus) {
267  // rotating, take passed time into account
268  auto deltaAngle = motorTimer.getTicksTillUp(time);
269  return (startAngle + deltaAngle) % TICKS_PER_ROTATION;
270  } else {
271  // not rotating, angle didn't change
272  return startAngle;
273  }
274 }
275 
276 void RealDrive::doSetMotor(bool status, EmuTime::param time)
277 {
278  if (!status) {
279  invalidateTrack(); // flush and ignore further writes
280  }
281 
282  startAngle = getCurrentAngle(time);
283  motorStatus = status;
284  motorTimer.advance(time);
285 
286  // TODO The following is a hack to emulate the drive LED behaviour.
287  // This should be moved to the FDC mapping code.
288  // TODO Each drive should get it's own independent LED.
289  motherBoard.getLedStatus().setLed(LedStatus::FDD, status);
290 }
291 
292 void RealDrive::setLoading(EmuTime::param time)
293 {
294  assert(motorStatus);
295  loadingIndicator.update(true);
296 
297  // ThrottleManager heuristic:
298  // We want to avoid getting stuck in 'loading state' when the MSX
299  // program forgets to turn off the motor.
300  syncLoadingTimeout.removeSyncPoint();
301  syncLoadingTimeout.setSyncPoint(time + EmuDuration::sec(1));
302 }
303 
304 void RealDrive::execLoadingTimeout()
305 {
306  loadingIndicator.update(false);
307 }
308 
309 void RealDrive::execMotorTimeout(EmuTime::param time)
310 {
311  doSetMotor(false, time);
312 }
313 
314 bool RealDrive::indexPulse(EmuTime::param time)
315 {
316  // Tested on real NMS8250:
317  // Only when there's a disk inserted and when the motor is spinning
318  // there are index pulses generated.
319  if (!(motorStatus && isDiskInserted())) {
320  return false;
321  }
322  return getCurrentAngle(time) < INDEX_DURATION;
323 }
324 
325 EmuTime RealDrive::getTimeTillIndexPulse(EmuTime::param time, int count)
326 {
327  if (!motorStatus || !isDiskInserted()) { // TODO is this correct?
328  return EmuTime::infinity();
329  }
330  unsigned delta = TICKS_PER_ROTATION - getCurrentAngle(time);
331  auto dur1 = MotorClock::duration(delta);
332  auto dur2 = MotorClock::duration(TICKS_PER_ROTATION) * (count - 1);
333  return time + dur1 + dur2;
334 }
335 
336 void RealDrive::invalidateTrack()
337 {
338  try {
339  flushTrack();
340  } catch (MSXException&) {
341  // ignore
342  }
343  trackValid = false;
344 }
345 
346 void RealDrive::getTrack()
347 {
348  if (!motorStatus) {
349  // cannot read track when disk isn't rotating
350  assert(!trackValid);
351  return;
352  }
353  if (!trackValid) {
354  if (auto rdTrack = getDiskReadTrack()) {
355  changer->getDisk().readTrack(*rdTrack, side, track);
356  } else {
357  track.clear(track.getLength());
358  }
359  trackValid = true;
360  trackDirty = false;
361  }
362 }
363 
365 {
366  getTrack();
367  return track.getLength();
368 }
369 
370 void RealDrive::writeTrackByte(int idx, byte val, bool addIdam)
371 {
372  getTrack();
373  // It's possible 'trackValid==false', but that's fine because in that
374  // case track won't be flushed to disk anyway.
375  track.write(idx, val, addIdam);
376  trackDirty = true;
377 }
378 
380 {
381  getTrack();
382  return trackValid ? track.read(idx) : 0;
383 }
384 
385 static constexpr unsigned divUp(unsigned a, unsigned b)
386 {
387  return (a + b - 1) / b;
388 }
389 EmuTime RealDrive::getNextSector(EmuTime::param time, RawTrack::Sector& sector)
390 {
391  getTrack();
392  int currentAngle = getCurrentAngle(time);
393  unsigned trackLen = track.getLength();
394  unsigned idx = divUp(currentAngle * trackLen, TICKS_PER_ROTATION);
395 
396  // 'addrIdx' points to the 'FE' byte in the 'A1 A1 A1 FE' sequence.
397  // This method also returns the moment in time when this 'FE' byte is
398  // located below the drive head. But when searching for the next sector
399  // header, the FDC needs to see this full sequence. So if the rotation
400  // distance is only 3 bytes or less we need to skip to the next sector
401  // header. IOW we need a sector header that's at least 4 bytes removed
402  // from the current position.
403  if (auto s = track.decodeNextSector(idx + 4)) {
404  sector = *s;
405  } else {
406  return EmuTime::infinity();
407  }
408  int sectorAngle = divUp(sector.addrIdx * TICKS_PER_ROTATION, trackLen);
409 
410  // note that if there is only one sector in this track, we have
411  // to do a full rotation.
412  int delta = sectorAngle - currentAngle;
413  if (delta < 4) delta += TICKS_PER_ROTATION;
414  assert(4 <= delta); assert(unsigned(delta) < (TICKS_PER_ROTATION + 4));
415 
416  return time + MotorClock::duration(delta);
417 }
418 
420 {
421  if (trackValid && trackDirty) {
422  if (auto wrTrack = getDiskWriteTrack()) {
423  changer->getDisk().writeTrack(*wrTrack, side, track);
424  }
425  trackDirty = false;
426  }
427 }
428 
430 {
431  return changer->diskChanged();
432 }
433 
435 {
436  return changer->peekDiskChanged();
437 }
438 
440 {
441  return false;
442 }
443 
445 {
447 }
448 
450 {
451  trackValid = false;
452 }
453 
454 
455 // version 1: initial version
456 // version 2: removed 'timeOut', added MOTOR_TIMEOUT schedulable
457 // version 3: added 'startAngle'
458 // version 4: removed 'userData' from Schedulable
459 // version 5: added 'track', 'trackValid', 'trackDirty'
460 // version 6: removed 'headLoadStatus' and 'headLoadTimer'
461 template<typename Archive>
462 void RealDrive::serialize(Archive& ar, unsigned version)
463 {
464  if (ar.versionAtLeast(version, 4)) {
465  ar.serialize("syncLoadingTimeout", syncLoadingTimeout,
466  "syncMotorTimeout", syncMotorTimeout);
467  } else {
468  Schedulable::restoreOld(ar, {&syncLoadingTimeout, &syncMotorTimeout});
469  }
470  ar.serialize("motorTimer", motorTimer,
471  "changer", *changer,
472  "headPos", headPos,
473  "side", side,
474  "motorStatus", motorStatus);
475  if (ar.versionAtLeast(version, 3)) {
476  ar.serialize("startAngle", startAngle);
477  } else {
478  assert(Archive::IS_LOADER);
479  startAngle = 0;
480  }
481  if (ar.versionAtLeast(version, 5)) {
482  ar.serialize("track", track);
483  ar.serialize("trackValid", trackValid);
484  ar.serialize("trackDirty", trackDirty);
485  }
486  if constexpr (Archive::IS_LOADER) {
487  // Right after a loadstate, the 'loading indicator' state may
488  // be wrong, but that's OK. It's anyway only a heuristic and
489  // it will be correct after at most one second.
490 
491  // This is a workaround for the fact that we can have multiple drives
492  // (and only one is on), in which case the 2nd drive will turn off the
493  // LED again which the first drive just turned on. TODO: fix by modelling
494  // individual drive LEDs properly. See also
495  // http://sourceforge.net/tracker/index.php?func=detail&aid=1540929&group_id=38274&atid=421864
496  if (motorStatus) {
497  motherBoard.getLedStatus().setLed(LedStatus::FDD, true);
498  }
499  }
500 }
502 
503 } // namespace openmsx
virtual void update(UpdateType type, std::string_view name, std::string_view value)=0
constexpr uint64_t getTicksTillUp(EmuTime::param e) const
Calculate the number of ticks this clock has to tick to reach or go past the given time.
Definition: Clock.hh:79
static constexpr EmuDuration duration(unsigned ticks)
Calculates the duration of the given number of ticks at this clock's frequency.
Definition: Clock.hh:35
constexpr void advance(EmuTime::param e)
Advance this clock in time until the last tick which is not past the given time.
Definition: Clock.hh:110
static constexpr EmuDuration sec(unsigned x)
Definition: EmuDuration.hh:39
void setLed(Led led, bool status)
Definition: LedStatus.cc:40
void update(bool newState)
Called by the device to indicate its loading state may have changed.
bool hasCommand(std::string_view command) const
MSXCommandController & getMSXCommandController()
std::shared_ptr< T > getSharedStuff(std::string_view name, Args &&...args)
Some MSX device parts are shared between several MSX devices (e.g.
byte read(int idx) const
Definition: RawTrack.hh:106
void write(int idx, byte val, bool setIdam=false)
Definition: RawTrack.cc:31
std::optional< Sector > decodeNextSector(unsigned startIdx) const
Get the next sector (starting from a certain index).
Definition: RawTrack.cc:132
void applyWd2793ReadTrackQuirk()
Definition: RawTrack.cc:190
void clear(unsigned size)
Clear track data.
Definition: RawTrack.cc:18
unsigned getLength() const
Get track length.
Definition: RawTrack.hh:99
This class implements a real drive, single or double sided.
Definition: RealDrive.hh:21
void applyWd2793ReadTrackQuirk() override
See RawTrack::applyWd2793ReadTrackQuirk()
Definition: RealDrive.cc:444
bool peekDiskChanged() const override
Definition: RealDrive.cc:434
void serialize(Archive &ar, unsigned version)
Definition: RealDrive.cc:462
bool isTrack00() const override
Head above track 0.
Definition: RealDrive.cc:191
void step(bool direction, EmuTime::param time) override
Step head.
Definition: RealDrive.cc:170
bool isDoubleSided() override
Is disk double sided?
Definition: RealDrive.cc:96
void invalidateWd2793ReadTrackQuirk() override
Definition: RealDrive.cc:449
void writeTrackByte(int idx, byte val, bool addIdam) override
Definition: RealDrive.cc:370
bool isWriteProtected() const override
Is disk write protected?
Definition: RealDrive.cc:88
bool getMotor() const override
Returns the previously set motor status.
Definition: RealDrive.cc:256
void setMotor(bool status, EmuTime::param time) override
Set motor on/off.
Definition: RealDrive.cc:201
void setSide(bool side) override
Side select.
Definition: RealDrive.cc:102
byte readTrackByte(int idx) override
Definition: RealDrive.cc:379
bool diskChanged() override
Is disk changed?
Definition: RealDrive.cc:429
bool indexPulse(EmuTime::param time) override
Gets the state of the index pulse.
Definition: RealDrive.cc:314
RealDrive(MSXMotherBoard &motherBoard, EmuDuration::param motorTimeout, bool signalsNeedMotorOn, bool doubleSided, DiskDrive::TrackMode trackMode)
Definition: RealDrive.cc:16
EmuTime getTimeTillIndexPulse(EmuTime::param time, int count) override
Return the time till the start of the next index pulse When there is no disk in the drive or when the...
Definition: RealDrive.cc:325
~RealDrive() override
Definition: RealDrive.cc:52
bool isDummyDrive() const override
Is there a dummy (unconnected) drive?
Definition: RealDrive.cc:439
void flushTrack() override
Definition: RealDrive.cc:419
bool isDiskInserted() const override
Is drive ready?
Definition: RealDrive.cc:69
EmuTime getNextSector(EmuTime::param time, RawTrack::Sector &sector) override
Definition: RealDrive.cc:389
unsigned getTrackLength() override
Definition: RealDrive.cc:364
bool getSide() const override
Definition: RealDrive.cc:108
static void restoreOld(Archive &ar, std::vector< Schedulable * > schedulables)
Definition: Schedulable.hh:77
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
Definition: lz4.cc:207
This file implemented 3 utility functions:
Definition: Autofire.cc:9
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:998
#define UNREACHABLE
Definition: unreachable.hh:38