OXYGENHUB
◆ main openmsx / src / network / SMXWiFi.cc
11.4 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "SMXWiFi.hh"
2
3
#include "Connector.hh"
4
#include "EmuDuration.hh"
5
#include "MSXCliComm.hh"
6
#include "MSXException.hh"
7
#include "RS232Device.hh"
8
#include "RS232Raw.hh"
9
#include "Timer.hh"
10
#include "serialize.hh"
11
12
#include <array>
13
#include <cassert>
14
#include <chrono>
15
#include <functional>
16
#include <memory>
17
18
namespace openmsx {
19
20
static constexpr auto baudRates = std::to_array<unsigned>({
21
	859372, 346520, 231014, 115200, 57600, 38400, 31250, 19200, 9600, 4800
22
});
23
24
SMXWiFi::SMXWiFi(DeviceConfig& config)
25
	: MSXDevice(config)
26
	, RS232Connector(MSXDevice::getPluggingController(), "smxwifi")
27
	, rom(MSXDevice::getName() + " ROM", "rom", config)
28
	, cpu(getCPU())
29
	, quickRcvSetting(config.getCommandController(),
30
		"smxwifi-emulatedesp32-quickrcv-enabled",
31
		"Use quick receive (assert WAIT when the UART FIFO is empty)", true)
32
{
33
	emulated = std::make_unique<EmulatedEsp32>(
34
		config, [this](std::span<const uint8_t> bytes) { pushToFifo(bytes); });
35
}
36
37
SMXWiFi::~SMXWiFi() = default;
38
39
// ====================================================================
40
//  Memory-mapped ROM
41
// ====================================================================
42
43
uint8_t SMXWiFi::peekMem(uint16_t address, EmuTime /*time*/) const
44
{
45
	return *getReadCacheLine(address);
46
}
47
48
uint8_t SMXWiFi::readMem(uint16_t address, EmuTime time)
49
{
50
	return peekMem(address, time);
51
}
52
53
const uint8_t* SMXWiFi::getReadCacheLine(uint16_t start) const
54
{
55
	if (0x4000 <= start && start < 0x8000) {
56
		return &rom[start & 0x3FFF];
57
	}
58
	return unmappedRead.data();
59
}
60
61
// powerDown: stop the emulated ESP32 while the MSX is powered off — it
62
// runs as a host thread with real network sockets, and on the real
63
// device the ESP32 is powered from the MSX bus, so it powers down with
64
// it. The destructor only covers device removal; power cycles happen
65
// without destruction. On power-up the default MSXDevice::powerUp()
66
// calls reset(), which resets and restarts the emulation.
67
void SMXWiFi::powerDown(EmuTime /*time*/)
68
{
69
	emulated->stop();
70
}
71
72
void SMXWiFi::reset(EmuTime time)
73
{
74
	uartSpeed = 0; // 859372
75
	resetFIFO();
76
	emulated->resetState();
77
	emulated->start();
78
	updateEndpoint(time);
79
	if (isRealEndpoint()) {
80
		// No endpoint switch happened, but the UART is back to its
81
		// default baud rate: re-apply the configuration to the port.
82
		applyParamsToReal(time);
83
	}
84
}
85
86
uint8_t SMXWiFi::readIO(uint16_t port, EmuTime time)
87
{
88
	switch (port & 0x01) {
89
	case 0: // UART data
90
		return readFIFO(time);
91
	case 1: // UART status
92
		return readStatus();
93
	default:
94
		return 0xFF;
95
	}
96
}
97
98
uint8_t SMXWiFi::peekIO(uint16_t port, EmuTime /*time*/) const
99
{
100
	switch (port & 0x01) {
101
	case 0: // UART data
102
		return peekFIFO();
103
	case 1: // UART status
104
		return peekStatus();
105
	default:
106
		return 0xFF;
107
	}
108
}
109
110
void SMXWiFi::writeIO(uint16_t port, uint8_t value, EmuTime time)
111
{
112
	switch (port & 0x01) {
113
	case 0: // UART command
114
		writeCommand(value, time);
115
		break;
116
	case 1: // UART data
117
		updateEndpoint(time);
118
		if (isRealEndpoint()) {
119
			getPluggedRS232Dev().recvByte(value, time);
120
		} else {
121
			emulated->recvByte(value, time);
122
		}
123
		break;
124
	}
125
}
126
127
void SMXWiFi::plug(Pluggable& device, EmuTime time)
128
{
129
	Connector::plug(device, time);
130
	// Configure the port right away when an RS232Raw is plugged, so the
131
	// first byte is always sent at the current UART speed (baud) and
132
	// format (8N1): on Windows this reopens the port before any traffic
133
	// flows, on POSIX the open handle is reconfigured live.
134
	if (dynamic_cast<RS232Raw*>(&getPluggedRS232Dev())) {
135
		applyParamsToReal(time);
136
	}
137
	updateEndpoint(time);
138
}
139
140
void SMXWiFi::unplug(EmuTime time)
141
{
142
	Connector::unplug(time);
143
	updateEndpoint(time);
144
}
145
146
void SMXWiFi::setDataBits(DataBits /*bits*/)
147
{
148
	// The SM-X WiFi UART has a fixed format (8 data bits, 1 stop bit, no
149
	// parity): only the baud rate is configurable (via the command
150
	// register), so these are no-ops. They are still required by the
151
	// RS232Connector interface.
152
}
153
154
void SMXWiFi::setStopBits(StopBits /*bits*/)
155
{
156
	// The SM-X WiFi UART has a fixed format (8 data bits, 1 stop bit, no
157
	// parity): only the baud rate is configurable (via the command
158
	// register), so these are no-ops. They are still required by the
159
	// RS232Connector interface.
160
}
161
162
void SMXWiFi::setParityBit(bool /*enable*/, Parity /*parity*/)
163
{
164
	// The SM-X WiFi UART has a fixed format (8 data bits, 1 stop bit, no
165
	// parity): only the baud rate is configurable (via the command
166
	// register), so these are no-ops. They are still required by the
167
	// RS232Connector interface.
168
}
169
170
void SMXWiFi::recvByte(uint8_t value, EmuTime /*time*/)
171
{
172
	// Only bytes from the active endpoint enter the FIFO: while a
173
	// plugged-but-inactive device (e.g. RS232Tester, or an RS232Raw whose
174
	// port is not open) is attached, the emulated ESP32 is the active
175
	// endpoint and bytes from the real side are dropped.
176
	if (!isRealEndpoint()) return;
177
	pushToFifo(std::span(&value, 1));
178
}
179
180
void SMXWiFi::pushToFifo(std::span<const uint8_t> data)
181
{
182
	std::scoped_lock lock(fifoMutex);
183
	fifo.push_back_range(data);
184
}
185
186
bool SMXWiFi::ready()
187
{
188
	return true;
189
}
190
191
bool SMXWiFi::acceptsData()
192
{
193
	return true;
194
}
195
196
bool SMXWiFi::directByteDelivery() const
197
{
198
	return true;
199
}
200
201
// Emulated time to stall the CPU (in slices) before declaring a FIFO
202
// underrun, and the slice size while waiting.
203
//
204
// This mirrors the real SM-X WiFi hardware: on an IN of the data port
205
// while the UART FIFO is empty, the device asserts WAIT, stalling the
206
// Z80 until a byte arrives or a timeout (25 ms on the real device)
207
// elapses, after which 0xFF is returned and the underrun bit is set.
208
// The driver (ESPUNAPI.asm) reads the response header status-guarded and
209
// then fires a blind INI/INIR for the announced payload size, which
210
// always outruns the UART stream; it detects the resulting underrun
211
// (status bit 4) after the loop and recovers via the 'r' retry command,
212
// which re-sends the whole response. So underruns are harmless by design
213
// and never corrupt the received data.
214
//
215
// The stall is done with cpu.wait() in small emulated-time slices: only
216
// the CPU is stopped, the rest of the machine (VDP, sound, timers, ...)
217
// keeps running, so other devices' emulation is not affected. The slice
218
// must be no larger than the fastest UART char time (11.6 us @ 859372
219
// baud) because cpu.wait() cannot be woken early: an arriving byte is
220
// only noticed at the next slice boundary.
221
//
222
// FIFO_TIMEOUT is 50 ms: more than the real device's 25 ms, because in
223
// the emulated setup the producer chain goes through a host USB-serial
224
// converter (adapter latency timer, driver buffering and packing),
225
// which adds gaps the real device (direct 3.3 V serial, no converter)
226
// does not have. 50 ms was found to cover that extra latency and keep
227
// the driver's retry path quiet during normal operation.
228
static constexpr auto FIFO_TIMEOUT = EmuDuration::msec(50);
229
static constexpr auto FIFO_POLL = EmuDuration::usec(12);
230
231
uint8_t SMXWiFi::readFIFO(EmuTime time)
232
{
233
	// Pop one byte if available (mutex-protected).
234
	auto tryPop = [this](uint8_t& v) {
235
		std::scoped_lock lock(fifoMutex);
236
		if (fifo.empty()) return false;
237
		v = fifo.front();
238
		fifo.pop_front();
239
		return true;
240
	};
241
	uint8_t v;
242
	if (tryPop(v)) return v;
243
	if (!quickRcvSetting.getBoolean()) {
244
		// Quick receive disabled: no WAIT stall on an empty FIFO; the
245
		// driver is told (status bit 3) that quick receive is
246
		// unsupported and polls the status register instead.
247
		return 0xFF;
248
	}
249
250
	// FIFO empty: give the producer (the emulated ESP32 thread or a real
251
	// ESP over the serial port) a short chance to provide data by
252
	// stalling only the CPU in emulated-time slices — the rest of the
253
	// machine keeps running.
254
	//
255
	// cpu.wait() only advances emulated time; the real time the stall
256
	// actually takes can be much less (a real ESP32 over the serial
257
	// port, or the emulated ESP32 waiting on host network data, needs
258
	// wall-clock time to deliver). Sleep the shortfall so the stall
259
	// keeps up with the real clock; if timer granularity overshoots,
260
	// the next iteration detects it and the emulation catches up again.
261
	auto deadline = time + FIFO_TIMEOUT;
262
	auto emuStart = time;
263
	auto realStart = std::chrono::steady_clock::now();
264
	while (time < deadline) {
265
		cpu.wait(time + FIFO_POLL);
266
		time = getCurrentTime();
267
		if (tryPop(v)) return v;
268
		auto target = realStart + std::chrono::duration<double>(
269
			(time - emuStart).toDouble());
270
		if (auto shortfall = target - std::chrono::steady_clock::now();
271
		    shortfall > std::chrono::microseconds(0)) {
272
			Timer::sleep(std::chrono::duration_cast<
273
			             std::chrono::microseconds>(shortfall).count());
274
		}
275
	}
276
277
	underrun = true;
278
	getCliComm().printInfo(
279
		"SMXWiFi: FIFO underrun: no data received within 50 ms");
280
	return 0xFF;
281
}
282
283
uint8_t SMXWiFi::peekFIFO() const
284
{
285
	std::scoped_lock lock(fifoMutex);
286
	return fifo.empty() ? 0xFF : fifo.front();
287
}
288
289
uint8_t SMXWiFi::readStatus()
290
{
291
	std::scoped_lock lock(fifoMutex);
292
	uint8_t status = peekStatusLocked();
293
	underrun = false;
294
	return status;
295
}
296
297
uint8_t SMXWiFi::peekStatusLocked() const
298
{
299
	uint8_t status = 0;
300
	if (!fifo.empty()) status |= 0x01;
301
	if (quickRcvSetting.getBoolean()) status |= 0x08; // quick receive
302
	if (underrun) status |= 0x10;
303
	return status;
304
}
305
306
uint8_t SMXWiFi::peekStatus() const
307
{
308
	std::scoped_lock lock(fifoMutex);
309
	return peekStatusLocked();
310
}
311
312
void SMXWiFi::writeCommand(uint8_t value, EmuTime time)
313
{
314
	updateEndpoint(time);
315
	if (value <= 9) {
316
		uartSpeed = value;
317
		if (isRealEndpoint()) {
318
			getPluggedRS232Dev().setBaudRate(baudRates[value]);
319
		} else {
320
			emulated->setBaudRate(baudRates[value]);
321
		}
322
	} else if (value == 20) {
323
		resetFIFO();
324
		emulated->resetFifo();
325
	}
326
}
327
328
void SMXWiFi::resetFIFO()
329
{
330
	std::scoped_lock lock(fifoMutex);
331
	fifo.clear();
332
	underrun = false;
333
}
334
335
// The UART talks to the real ESP32 only when the connector holds an
336
// RS232Raw whose host serial port is actually open; otherwise the
337
// emulated ESP32 is the active endpoint: nothing is plugged, a
338
// non-RS232Raw device is plugged, or the port could not be opened (no
339
// port selected, port in use, ...).
340
bool SMXWiFi::isRealEndpoint() const
341
{
342
	auto* raw = dynamic_cast<const RS232Raw*>(&getPluggedRS232Dev());
343
	return raw && raw->isOpen();
344
}
345
346
// Re-evaluates the active endpoint and performs the switch when it
347
// changed: stale bytes are flushed, the emulated ESP32 is reset (it
348
// always starts clean after a migration) and, when switching to the real
349
// endpoint, the current UART configuration is pushed to the serial device
350
// (the RS232Raw port may have opened with its own defaults).
351
void SMXWiFi::updateEndpoint(EmuTime time)
352
{
353
	bool real = isRealEndpoint();
354
	if (real == realEndpointActive) return;
355
	realEndpointActive = real;
356
	resetFIFO();
357
	emulated->resetState();
358
	if (real) {
359
		applyParamsToReal(time);
360
	}
361
}
362
363
// Pushes the current UART configuration to the plugged device: the baud
364
// rate (configurable via the command register) and the fixed format
365
// 8N1 with DTR/RTS deasserted.
366
void SMXWiFi::applyParamsToReal(EmuTime time) const
367
{
368
	auto& dev = getPluggedRS232Dev();
369
	dev.setBaudRate(baudRates[uartSpeed]);
370
	dev.setDataBits(DataBits::D8);
371
	dev.setStopBits(StopBits::S1);
372
	dev.setParityBit(false, Parity::EVEN);
373
	dev.setDTR(false, time);
374
	dev.setRTS(false, time);
375
}
376
377
template<typename Archive>
378
void SMXWiFi::serialize(Archive& ar, unsigned /*version*/)
379
{
380
	ar.template serializeBase<MSXDevice>(*this);
381
	ar.template serializeBase<RS232Connector>(*this);
382
	ar.serialize("uartSpeed", uartSpeed);
383
	if constexpr (Archive::IS_LOADER) {
384
		resetFIFO();
385
		emulated->resetState();
386
		updateEndpoint(EmuTime::zero());
387
	}
388
}
389
INSTANTIATE_SERIALIZE_METHODS(SMXWiFi);
390
REGISTER_MSXDEVICE(SMXWiFi, "SMXWiFi");
391
392
} // namespace openmsx