openMSX
RealDrive.cc
Go to the documentation of this file.
1 #include "RealDrive.hh"
2 #include "Disk.hh"
3 #include "DiskChanger.hh"
4 #include "MSXMotherBoard.hh"
5 #include "Reactor.hh"
6 #include "LedStatus.hh"
7 #include "CommandController.hh"
8 #include "CliComm.hh"
9 #include "GlobalSettings.hh"
10 #include "MSXException.hh"
11 #include "serialize.hh"
12 #include <memory>
13 
14 using std::string;
15 
16 namespace openmsx {
17 
19  bool signalsNeedMotorOn_, bool doubleSided)
20  : syncLoadingTimeout(motherBoard_.getScheduler())
21  , syncMotorTimeout (motherBoard_.getScheduler())
22  , motherBoard(motherBoard_)
23  , loadingIndicator(
24  motherBoard.getReactor().getGlobalSettings().getThrottleManager())
25  , motorTimeout(motorTimeout_)
26  , motorTimer(getCurrentTime())
27  , headPos(0), side(0), startAngle(0)
28  , motorStatus(false)
29  , doubleSizedDrive(doubleSided)
30  , signalsNeedMotorOn(signalsNeedMotorOn_)
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  string driveName = "diskX"; driveName[4] = char('a' + i);
43 
44  if (motherBoard.getCommandController().hasCommand(driveName)) {
45  throw MSXException("Duplicated drive name: ", driveName);
46  }
47  motherBoard.getMSXCliComm().update(CliComm::HARDWARE, driveName, "add");
48  changer = std::make_unique<DiskChanger>(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 void RealDrive::step(bool direction, EmuTime::param time)
114 {
115  invalidateTrack();
116 
117  if (direction) {
118  // step in
119  if (headPos < MAX_TRACK) {
120  headPos++;
121  }
122  } else {
123  // step out
124  if (headPos > 0) {
125  headPos--;
126  }
127  }
128  // ThrottleManager heuristic:
129  // If the motor is turning and there is head movement, assume the
130  // MSX program is (still) loading/saving to disk
131  if (motorStatus) setLoading(time);
132 }
133 
135 {
136  // On a Philips-NMS8280 the track00 signal is never active when the
137  // drive motor is turned off. On a National-FS-5500F2 the motor status
138  // doesn't matter, the single/dual drive detection routine (during
139  // the MSX boot sequence) even depends on this signal with motor off.
140  if (signalsNeedMotorOn && !motorStatus) return false;
141  return headPos == 0;
142 }
143 
144 void RealDrive::setMotor(bool status, EmuTime::param time)
145 {
146  // If status = true, motor is immediately turned on. If status = false,
147  // the motor is only turned off after some (configurable) amount of
148  // time (can be zero). Let's call the last passed status parameter the
149  // 'logical' motor status.
150  //
151  // Loading indicator heuristic:
152  // Loading indicator only reacts to _changes_ in the _logical_ motor
153  // status. So when the motor is turned off, we immediately assume the
154  // MSX program is done loading (or saving) (we don't wait for the motor
155  // timeout). Turning the motor on when it already was logically on has
156  // no effect. But turning it back on while it was logically off but
157  // still in the motor-off-timeout phase does reset the loading
158  // indicator.
159  //
160  if (status) {
161  // (Try to) remove scheduled action to turn motor off.
162  if (syncMotorTimeout.removeSyncPoint()) {
163  // If there actually was such an action scheduled, we
164  // need to turn on the loading indicator.
165  assert(motorStatus);
166  setLoading(time);
167  return;
168  }
169  if (motorStatus) {
170  // Motor was still turning, we're done.
171  // Note: no effect on loading indicator.
172  return;
173  }
174  // Actually turn motor on (it was off before).
175  doSetMotor(true, time);
176  setLoading(time);
177  } else {
178  if (!motorStatus) {
179  // Motor was already off, we're done.
180  return;
181  }
182  if (syncMotorTimeout.pendingSyncPoint()) {
183  // We had already scheduled an action to turn the motor
184  // off, we're done.
185  return;
186  }
187  // Heuristic:
188  // Immediately react to 'logical' motor status, even if the
189  // motor will (possibly) still keep rotating for a few
190  // seconds.
191  syncLoadingTimeout.removeSyncPoint();
192  loadingIndicator.update(false);
193 
194  // Turn the motor off after some timeout (timeout could be 0)
195  syncMotorTimeout.setSyncPoint(time + motorTimeout);
196  }
197 }
198 
200 {
201  // note: currently unused because of the implementation in DriveMultiplexer
202  // note: currently returns the actual motor status, could be differnt from the
203  // last set status because of 'syncMotorTimeout'.
204  return motorStatus;
205 }
206 
207 unsigned RealDrive::getCurrentAngle(EmuTime::param time) const
208 {
209  if (motorStatus) {
210  // rotating, take passed time into account
211  auto deltaAngle = motorTimer.getTicksTillUp(time);
212  return (startAngle + deltaAngle) % TICKS_PER_ROTATION;
213  } else {
214  // not rotating, angle didn't change
215  return startAngle;
216  }
217 }
218 
219 void RealDrive::doSetMotor(bool status, EmuTime::param time)
220 {
221  if (!status) {
222  invalidateTrack(); // flush and ignore further writes
223  }
224 
225  startAngle = getCurrentAngle(time);
226  motorStatus = status;
227  motorTimer.advance(time);
228 
229  // TODO The following is a hack to emulate the drive LED behaviour.
230  // This should be moved to the FDC mapping code.
231  // TODO Each drive should get it's own independent LED.
232  motherBoard.getLedStatus().setLed(LedStatus::FDD, status);
233 }
234 
235 void RealDrive::setLoading(EmuTime::param time)
236 {
237  assert(motorStatus);
238  loadingIndicator.update(true);
239 
240  // ThrottleManager heuristic:
241  // We want to avoid getting stuck in 'loading state' when the MSX
242  // program forgets to turn off the motor.
243  syncLoadingTimeout.removeSyncPoint();
244  syncLoadingTimeout.setSyncPoint(time + EmuDuration::sec(1));
245 }
246 
247 void RealDrive::execLoadingTimeout()
248 {
249  loadingIndicator.update(false);
250 }
251 
252 void RealDrive::execMotorTimeout(EmuTime::param time)
253 {
254  doSetMotor(false, time);
255 }
256 
257 bool RealDrive::indexPulse(EmuTime::param time)
258 {
259  // Tested on real NMS8250:
260  // Only when there's a disk inserted and when the motor is spinning
261  // there are index pulses generated.
262  if (!(motorStatus && isDiskInserted())) {
263  return false;
264  }
265  return getCurrentAngle(time) < INDEX_DURATION;
266 }
267 
268 EmuTime RealDrive::getTimeTillIndexPulse(EmuTime::param time, int count)
269 {
270  if (!motorStatus || !isDiskInserted()) { // TODO is this correct?
271  return EmuTime::infinity();
272  }
273  unsigned delta = TICKS_PER_ROTATION - getCurrentAngle(time);
274  auto dur1 = MotorClock::duration(delta);
275  auto dur2 = MotorClock::duration(TICKS_PER_ROTATION) * (count - 1);
276  return time + dur1 + dur2;
277 }
278 
279 void RealDrive::invalidateTrack()
280 {
281  try {
282  flushTrack();
283  } catch (MSXException&) {
284  // ignore
285  }
286  trackValid = false;
287 }
288 
289 void RealDrive::getTrack()
290 {
291  if (!motorStatus) {
292  // cannot read track when disk isn't rotating
293  assert(!trackValid);
294  return;
295  }
296  if (!trackValid) {
297  changer->getDisk().readTrack(headPos, side, track);
298  trackValid = true;
299  trackDirty = false;
300  }
301 }
302 
304 {
305  getTrack();
306  return track.getLength();
307 }
308 
309 void RealDrive::writeTrackByte(int idx, byte val, bool addIdam)
310 {
311  getTrack();
312  // It's possible 'trackValid==false', but that's fine because in that
313  // case track won't be flushed to disk anyway.
314  track.write(idx, val, addIdam);
315  trackDirty = true;
316 }
317 
319 {
320  getTrack();
321  return trackValid ? track.read(idx) : 0;
322 }
323 
324 static inline unsigned divUp(unsigned a, unsigned b)
325 {
326  return (a + b - 1) / b;
327 }
328 EmuTime RealDrive::getNextSector(EmuTime::param time, RawTrack::Sector& sector)
329 {
330  getTrack();
331  int currentAngle = getCurrentAngle(time);
332  unsigned trackLen = track.getLength();
333  unsigned idx = divUp(currentAngle * trackLen, TICKS_PER_ROTATION);
334 
335  // 'addrIdx' points to the 'FE' byte in the 'A1 A1 A1 FE' sequence.
336  // This method also returns the moment in time when this 'FE' byte is
337  // located below the drive head. But when searching for the next sector
338  // header, the FDC needs to see this full sequence. So if the rotation
339  // distance is only 3 bytes or less we need to skip to the next sector
340  // header. IOW we need a sector header that's at least 4 bytes removed
341  // from the current position.
342  if (!track.decodeNextSector(idx + 4, sector)) {
343  return EmuTime::infinity();
344  }
345  int sectorAngle = divUp(sector.addrIdx * TICKS_PER_ROTATION, trackLen);
346 
347  // note that if there is only one sector in this track, we have
348  // to do a full rotation.
349  int delta = sectorAngle - currentAngle;
350  if (delta < 4) delta += TICKS_PER_ROTATION;
351  assert(4 <= delta); assert(unsigned(delta) < (TICKS_PER_ROTATION + 4));
352 
353  return time + MotorClock::duration(delta);
354 }
355 
357 {
358  if (trackValid && trackDirty) {
359  changer->getDisk().writeTrack(headPos, side, track);
360  trackDirty = false;
361  }
362 }
363 
365 {
366  return changer->diskChanged();
367 }
368 
370 {
371  return changer->peekDiskChanged();
372 }
373 
375 {
376  return false;
377 }
378 
380 {
382 }
383 
385 {
386  trackValid = false;
387 }
388 
389 
390 // version 1: initial version
391 // version 2: removed 'timeOut', added MOTOR_TIMEOUT schedulable
392 // version 3: added 'startAngle'
393 // version 4: removed 'userData' from Schedulable
394 // version 5: added 'track', 'trackValid', 'trackDirty'
395 // version 6: removed 'headLoadStatus' and 'headLoadTimer'
396 template<typename Archive>
397 void RealDrive::serialize(Archive& ar, unsigned version)
398 {
399  if (ar.versionAtLeast(version, 4)) {
400  ar.serialize("syncLoadingTimeout", syncLoadingTimeout,
401  "syncMotorTimeout", syncMotorTimeout);
402  } else {
403  Schedulable::restoreOld(ar, {&syncLoadingTimeout, &syncMotorTimeout});
404  }
405  ar.serialize("motorTimer", motorTimer,
406  "changer", *changer,
407  "headPos", headPos,
408  "side", side,
409  "motorStatus", motorStatus);
410  if (ar.versionAtLeast(version, 3)) {
411  ar.serialize("startAngle", startAngle);
412  } else {
413  assert(ar.isLoader());
414  startAngle = 0;
415  }
416  if (ar.versionAtLeast(version, 5)) {
417  ar.serialize("track", track);
418  ar.serialize("trackValid", trackValid);
419  ar.serialize("trackDirty", trackDirty);
420  }
421  if (ar.isLoader()) {
422  // Right after a loadstate, the 'loading indicator' state may
423  // be wrong, but that's OK. It's anyway only a heuristic and
424  // it will be correct after at most one second.
425 
426  // This is a workaround for the fact that we can have multiple drives
427  // (and only one is on), in which case the 2nd drive will turn off the
428  // LED again which the first drive just turned on. TODO: fix by modelling
429  // individual drive LEDs properly. See also
430  // http://sourceforge.net/tracker/index.php?func=detail&aid=1540929&group_id=38274&atid=421864
431  if (motorStatus) {
432  motherBoard.getLedStatus().setLed(LedStatus::FDD, true);
433  }
434  }
435 }
437 
438 } // namespace openmsx
virtual void update(UpdateType type, std::string_view name, std::string_view value)=0
void applyWd2793ReadTrackQuirk()
Definition: RawTrack.cc:188
bool diskChanged() override
Is disk changed?
Definition: RealDrive.cc:364
virtual bool hasCommand(std::string_view command) const =0
Does a command with this name already exist?
unsigned getLength() const
Get track length.
Definition: RawTrack.hh:96
byte readTrackByte(int idx) override
Definition: RealDrive.cc:318
unsigned getTrackLength() override
Definition: RealDrive.cc:303
bool isDoubleSided() const override
Is disk double sided?
Definition: RealDrive.cc:96
byte read(int idx) const
Definition: RawTrack.hh:103
uint8_t byte
8 bit unsigned integer
Definition: openmsx.hh:26
bool isWriteProtected() const override
Is disk write protected?
Definition: RealDrive.cc:88
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
~RealDrive() override
Definition: RealDrive.cc:52
static constexpr EmuDuration duration(unsigned ticks)
Calculates the duration of the given number of ticks at this clock&#39;s frequency.
Definition: Clock.hh:35
bool indexPulse(EmuTime::param time) override
Gets the state of the index pulse.
Definition: RealDrive.cc:257
RealDrive(MSXMotherBoard &motherBoard, EmuDuration::param motorTimeout, bool signalsNeedMotorOn, bool doubleSided)
Definition: RealDrive.cc:18
void serialize(Archive &ar, unsigned version)
Definition: RealDrive.cc:397
static void restoreOld(Archive &ar, std::vector< Schedulable *> schedulables)
Definition: Schedulable.hh:77
bool peekDiskChanged() const override
Definition: RealDrive.cc:369
bool isDiskInserted() const override
Is drive ready?
Definition: RealDrive.cc:69
void update(bool newState)
Called by the device to indicate its loading state may have changed.
bool getMotor() const override
Returns the previously set motor status.
Definition: RealDrive.cc:199
void step(bool direction, EmuTime::param time) override
Step head.
Definition: RealDrive.cc:113
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
Definition: lz4.cc:207
bool decodeNextSector(unsigned startIdx, Sector &sector) const
Get the next sector (starting from a certain index).
Definition: RawTrack.cc:131
bool isTrack00() const override
Head above track 0.
Definition: RealDrive.cc:134
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
CommandController & getCommandController()
EmuTime getNextSector(EmuTime::param time, RawTrack::Sector &sector) override
Definition: RealDrive.cc:328
void flushTrack() override
Definition: RealDrive.cc:356
void applyWd2793ReadTrackQuirk() override
See RawTrack::applyWd2793ReadTrackQuirk()
Definition: RealDrive.cc:379
void setMotor(bool status, EmuTime::param time) override
Set motor on/off.
Definition: RealDrive.cc:144
bool isDummyDrive() const override
Is there a dummy (unconncted) drive?
Definition: RealDrive.cc:374
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:268
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:981
void setSide(bool side) override
Side select.
Definition: RealDrive.cc:102
std::shared_ptr< T > getSharedStuff(std::string_view name, Args &&...args)
Some MSX device parts are shared between several MSX devices (e.g.
void writeTrackByte(int idx, byte val, bool addIdam) override
Definition: RealDrive.cc:309
void invalidateWd2793ReadTrackQuirk() override
Definition: RealDrive.cc:384
bool getSide() const override
Definition: RealDrive.cc:108
void setLed(Led led, bool status)
Definition: LedStatus.cc:39
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
void write(int idx, byte val, bool setIdam=false)
Definition: RawTrack.cc:30
This class implements a real drive, single or double sided.
Definition: RealDrive.hh:20