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