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