OXYGENHUB
◆ main openmsx / src / serial / MSXMidi.cc
10.6 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "MSXMidi.hh"
2
3
#include "MidiInDevice.hh"
4
5
#include "MSXCPUInterface.hh"
6
#include "MSXException.hh"
7
#include "serialize.hh"
8
9
#include "narrow.hh"
10
#include "outer.hh"
11
#include "unreachable.hh"
12
#include "xrange.hh"
13
14
#include <cassert>
15
16
namespace openmsx {
17
18
// Documented in MSX-Datapack Vol. 3, section 4 (MSX-MIDI), from page 634
19
static constexpr uint8_t LIMITED_RANGE_VALUE = 0x01; // b0 = "E8" => determines port range
20
static constexpr uint8_t DISABLED_VALUE      = 0x80; // b7 = EN
21
22
MSXMidi::MSXMidi(const DeviceConfig& config)
23
	: MSXDevice(config)
24
	, MidiInConnector(MSXDevice::getPluggingController(), "MSX-MIDI-in")
25
	, timerIRQ(getMotherBoard(), MSXDevice::getName() + ".IRQtimer")
26
	, rxrdyIRQ(getMotherBoard(), MSXDevice::getName() + ".IRQrxrdy")
27
	, isExternalMSXMIDI(config.findChild("external") != nullptr)
28
	, isEnabled(!isExternalMSXMIDI)
29
	, outConnector(MSXDevice::getPluggingController(), "MSX-MIDI-out")
30
	, i8251(getScheduler(), interface, getCurrentTime())
31
	, i8254(getScheduler(), &cntr0, nullptr, &cntr2, getCurrentTime())
32
{
33
	EmuDuration total = EmuDuration::hz(4e6); // 4MHz
34
	EmuDuration hi    = EmuDuration::hz(8e6); // 8MHz half clock period
35
	EmuTime time = getCurrentTime();
36
	i8254.getClockPin(0).setPeriodicState(total, hi, time);
37
	i8254.getClockPin(1).setState(false, time);
38
	i8254.getClockPin(2).setPeriodicState(total, hi, time);
39
	i8254.getOutputPin(2).generateEdgeSignals(true, time);
40
	reset(time);
41
42
	if (isExternalMSXMIDI) {
43
		// Ports are dynamically registered.
44
		if (config.findChild("io")) {
45
			throw MSXException(
46
				"Bad MSX-MIDI configuration, when using "
47
				"'external', you cannot specify I/O ports!");
48
		}
49
		// Out-port 0xE2 is always enabled.
50
		getCPUInterface().register_IO_Out(0xE2, this);
51
		// Not enabled, so no other ports need to be registered yet.
52
		assert(!isEnabled);
53
	} else {
54
		// Ports are statically registered (via the config file).
55
	}
56
}
57
58
MSXMidi::~MSXMidi()
59
{
60
	if (isExternalMSXMIDI) {
61
		registerIOports(DISABLED_VALUE | LIMITED_RANGE_VALUE); // disabled, unregisters ports
62
		getCPUInterface().unregister_IO_Out(0xE2, this);
63
	}
64
}
65
66
void MSXMidi::reset(EmuTime time)
67
{
68
	timerIRQlatch = false;
69
	timerIRQenabled = false;
70
	timerIRQ.reset();
71
	rxrdyIRQlatch = false;
72
	rxrdyIRQenabled = false;
73
	rxrdyIRQ.reset();
74
75
	if (isExternalMSXMIDI) {
76
		registerIOports(DISABLED_VALUE | LIMITED_RANGE_VALUE); // also resets state
77
	}
78
	i8251.reset(time);
79
}
80
81
uint8_t MSXMidi::readIO(uint16_t port, EmuTime time)
82
{
83
	// If not enabled then no ports should have been registered.
84
	assert(isEnabled);
85
86
	// This handles both ports in range 0xE0-0xE1 and 0xE8-0xEF
87
	// Depending on the value written to 0xE2 they are active or not
88
	switch (port & 7) {
89
		case 0: // UART data register
90
		case 1: // UART status register
91
			return i8251.readIO(port & 1, time);
92
		case 2: // timer interrupt flag off
93
		case 3: // no function
94
			return 0xFF;
95
		case 4: // counter 0 data port
96
		case 5: // counter 1 data port
97
		case 6: // counter 2 data port
98
		case 7: // timer command register
99
			return i8254.readIO(port & 3, time);
100
		default:
101
			UNREACHABLE;
102
	}
103
}
104
105
uint8_t MSXMidi::peekIO(uint16_t port, EmuTime time) const
106
{
107
	// If not enabled then no ports should have been registered.
108
	assert(isEnabled);
109
110
	// This handles both ports in range 0xE0-0xE1 and 0xE8-0xEF
111
	// Depending on the value written to 0xE2 they are active or not
112
	switch (port & 7) {
113
		case 0: // UART data register
114
		case 1: // UART status register
115
			return i8251.peekIO(port & 1, time);
116
		case 2: // timer interrupt flag off
117
		case 3: // no function
118
			return 0xFF;
119
		case 4: // counter 0 data port
120
		case 5: // counter 1 data port
121
		case 6: // counter 2 data port
122
		case 7: // timer command register
123
			return i8254.peekIO(port & 3, time);
124
		default:
125
			UNREACHABLE;
126
	}
127
}
128
129
void MSXMidi::writeIO(uint16_t port, uint8_t value, EmuTime time)
130
{
131
	if (isExternalMSXMIDI && ((port & 0xFF) == 0xE2)) {
132
		// control register
133
		registerIOports(value);
134
		return;
135
	}
136
	assert(isEnabled);
137
138
	// This handles both ports in range 0xE0-0xE1 and 0xE8-0xEF
139
	switch (port & 7) {
140
		case 0: // UART data register
141
		case 1: // UART command register
142
			i8251.writeIO(port & 1, value, time);
143
			break;
144
		case 2: // timer interrupt flag off
145
			setTimerIRQ(false, time);
146
			break;
147
		case 3: // no function
148
			break;
149
		case 4: // counter 0 data port
150
		case 5: // counter 1 data port
151
		case 6: // counter 2 data port
152
		case 7: // timer command register
153
			i8254.writeIO(port & 3, value, time);
154
			break;
155
	}
156
}
157
158
void MSXMidi::registerIOports(uint8_t value)
159
{
160
	assert(isExternalMSXMIDI);
161
	bool newIsEnabled = (value & DISABLED_VALUE) == 0;
162
	bool newIsLimited = (value & LIMITED_RANGE_VALUE) != 0;
163
164
	auto& cpuInterface = getCPUInterface();
165
	if (newIsEnabled != isEnabled) {
166
		// Enable/disabled status changes, possibly limited status
167
		// changes as well but that doesn't matter, we anyway need
168
		// to (un)register the full port range.
169
		if (newIsEnabled) {
170
			// disabled -> enabled
171
			if (newIsLimited) {
172
				cpuInterface.register_IO_InOut_range(0xE0, 2, this);
173
			} else {
174
				cpuInterface.register_IO_InOut_range(0xE8, 8, this);
175
			}
176
		} else {
177
			// enabled -> disabled
178
			if (isLimitedTo8251) { // note: old isLimited status
179
				cpuInterface.unregister_IO_InOut_range(0xE0, 2, this);
180
			} else {
181
				cpuInterface.unregister_IO_InOut_range(0xE8, 8, this);
182
			}
183
		}
184
185
	} else if (isEnabled && (newIsLimited != isLimitedTo8251)) {
186
		// Remains enabled, and only 'isLimited' status changes.
187
		// Need to switch between the low/high range.
188
		if (newIsLimited) {
189
			// Switch high->low range.
190
			cpuInterface.unregister_IO_InOut_range(0xE8, 8, this);
191
			cpuInterface.register_IO_InOut_range  (0xE0, 2, this);
192
		} else {
193
			// Switch low->high range.
194
			cpuInterface.unregister_IO_InOut_range(0xE0, 2, this);
195
			cpuInterface.register_IO_InOut_range  (0xE8, 8, this);
196
		}
197
	}
198
199
	isEnabled       = newIsEnabled;
200
	isLimitedTo8251 = newIsLimited;
201
}
202
203
void MSXMidi::setTimerIRQ(bool status, EmuTime time)
204
{
205
	if (timerIRQlatch != status) {
206
		timerIRQlatch = status;
207
		if (timerIRQenabled) {
208
			timerIRQ.set(timerIRQlatch);
209
		}
210
		updateEdgeEvents(time);
211
	}
212
}
213
214
void MSXMidi::enableTimerIRQ(bool enabled, EmuTime time)
215
{
216
	if (timerIRQenabled != enabled) {
217
		timerIRQenabled = enabled;
218
		if (timerIRQlatch) {
219
			timerIRQ.set(timerIRQenabled);
220
		}
221
		updateEdgeEvents(time);
222
	}
223
}
224
225
void MSXMidi::updateEdgeEvents(EmuTime time)
226
{
227
	bool wantEdges = timerIRQenabled && !timerIRQlatch;
228
	i8254.getOutputPin(2).generateEdgeSignals(wantEdges, time);
229
}
230
231
void MSXMidi::setRxRDYIRQ(bool status)
232
{
233
	if (rxrdyIRQlatch != status) {
234
		rxrdyIRQlatch = status;
235
		if (rxrdyIRQenabled) {
236
			rxrdyIRQ.set(rxrdyIRQlatch);
237
		}
238
	}
239
}
240
241
void MSXMidi::enableRxRDYIRQ(bool enabled)
242
{
243
	if (rxrdyIRQenabled != enabled) {
244
		rxrdyIRQenabled = enabled;
245
		if (!rxrdyIRQenabled && rxrdyIRQlatch) {
246
			rxrdyIRQ.reset();
247
		}
248
	}
249
}
250
251
252
// I8251Interface  (pass calls from I8251 to outConnector)
253
254
void MSXMidi::Interface::setRxRDY(bool status, EmuTime /*time*/)
255
{
256
	auto& midi = OUTER(MSXMidi, interface);
257
	midi.setRxRDYIRQ(status);
258
}
259
260
void MSXMidi::Interface::setDTR(bool status, EmuTime time)
261
{
262
	auto& midi = OUTER(MSXMidi, interface);
263
	midi.enableTimerIRQ(status, time);
264
}
265
266
void MSXMidi::Interface::setRTS(bool status, EmuTime /*time*/)
267
{
268
	auto& midi = OUTER(MSXMidi, interface);
269
	midi.enableRxRDYIRQ(status);
270
}
271
272
bool MSXMidi::Interface::getDSR(EmuTime /*time*/)
273
{
274
	const auto& midi = OUTER(MSXMidi, interface);
275
	return midi.timerIRQ.getState();
276
}
277
278
bool MSXMidi::Interface::getCTS(EmuTime /*time*/)
279
{
280
	return true;
281
}
282
283
void MSXMidi::Interface::setDataBits(DataBits bits)
284
{
285
	auto& midi = OUTER(MSXMidi, interface);
286
	midi.outConnector.setDataBits(bits);
287
}
288
289
void MSXMidi::Interface::setStopBits(StopBits bits)
290
{
291
	auto& midi = OUTER(MSXMidi, interface);
292
	midi.outConnector.setStopBits(bits);
293
}
294
295
void MSXMidi::Interface::setParityBit(bool enable, Parity parity)
296
{
297
	auto& midi = OUTER(MSXMidi, interface);
298
	midi.outConnector.setParityBit(enable, parity);
299
}
300
301
void MSXMidi::Interface::recvByte(uint8_t value, EmuTime time)
302
{
303
	auto& midi = OUTER(MSXMidi, interface);
304
	midi.outConnector.recvByte(value, time);
305
}
306
307
void MSXMidi::Interface::signal(EmuTime time)
308
{
309
	const auto& midi = OUTER(MSXMidi, interface);
310
	midi.getPluggedMidiInDev().signal(time);
311
}
312
313
314
// Counter 0 output
315
316
void MSXMidi::Counter0::signal(ClockPin& pin, EmuTime time)
317
{
318
	auto& midi = OUTER(MSXMidi, cntr0);
319
	ClockPin& clk = midi.i8251.getClockPin();
320
	if (pin.isPeriodic()) {
321
		clk.setPeriodicState(pin.getTotalDuration(),
322
		                     pin.getHighDuration(), time);
323
	} else {
324
		clk.setState(pin.getState(time), time);
325
	}
326
}
327
328
void MSXMidi::Counter0::signalPosEdge(ClockPin& /*pin*/, EmuTime /*time*/)
329
{
330
	UNREACHABLE;
331
}
332
333
334
// Counter 2 output
335
336
void MSXMidi::Counter2::signal(ClockPin& pin, EmuTime time)
337
{
338
	auto& midi = OUTER(MSXMidi, cntr2);
339
	ClockPin& clk = midi.i8254.getClockPin(1);
340
	if (pin.isPeriodic()) {
341
		clk.setPeriodicState(pin.getTotalDuration(),
342
		                     pin.getHighDuration(), time);
343
	} else {
344
		clk.setState(pin.getState(time), time);
345
	}
346
}
347
348
void MSXMidi::Counter2::signalPosEdge(ClockPin& /*pin*/, EmuTime time)
349
{
350
	auto& midi = OUTER(MSXMidi, cntr2);
351
	midi.setTimerIRQ(true, time);
352
}
353
354
355
// MidiInConnector
356
357
bool MSXMidi::ready()
358
{
359
	return i8251.isRecvReady();
360
}
361
362
bool MSXMidi::acceptsData()
363
{
364
	return i8251.isRecvEnabled();
365
}
366
367
void MSXMidi::setDataBits(DataBits bits)
368
{
369
	i8251.setDataBits(bits);
370
}
371
372
void MSXMidi::setStopBits(StopBits bits)
373
{
374
	i8251.setStopBits(bits);
375
}
376
377
void MSXMidi::setParityBit(bool enable, Parity parity)
378
{
379
	i8251.setParityBit(enable, parity);
380
}
381
382
void MSXMidi::recvByte(uint8_t value, EmuTime time)
383
{
384
	i8251.recvByte(value, time);
385
}
386
387
388
template<typename Archive>
389
void MSXMidi::serialize(Archive& ar, unsigned version)
390
{
391
	ar.template serializeBase<MSXDevice>(*this);
392
393
	ar.template serializeBase<MidiInConnector>(*this);
394
	ar.serialize("outConnector",    outConnector,
395
	             "timerIRQ",        timerIRQ,
396
	             "rxrdyIRQ",        rxrdyIRQ,
397
	             "timerIRQlatch",   timerIRQlatch,
398
	             "timerIRQenabled", timerIRQenabled,
399
	             "rxrdyIRQlatch",   rxrdyIRQlatch,
400
	             "rxrdyIRQenabled", rxrdyIRQenabled,
401
	             "I8251",           i8251,
402
	             "I8254",           i8254);
403
	if (ar.versionAtLeast(version, 2)) {
404
		bool newIsEnabled = isEnabled; // copy for saver
405
		bool newIsLimitedTo8251 = isLimitedTo8251; // copy for saver
406
		ar.serialize("isEnabled",       newIsEnabled,
407
		             "isLimitedTo8251", newIsLimitedTo8251);
408
		if constexpr (Archive::IS_LOADER) {
409
			if (isExternalMSXMIDI) {
410
				registerIOports((newIsEnabled ? 0x00 : DISABLED_VALUE) | (newIsLimitedTo8251 ? LIMITED_RANGE_VALUE : 0x00));
411
			}
412
		}
413
	}
414
415
	// don't serialize:  cntr0, cntr2, interface
416
}
417
INSTANTIATE_SERIALIZE_METHODS(MSXMidi);
418
REGISTER_MSXDEVICE(MSXMidi, "MSX-Midi");
419
420
} // namespace openmsx