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