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