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