| 1 |
#include "MSXPiDevice.hh" |
| 2 |
|
| 3 |
#include "DeviceConfig.hh" |
| 4 |
#include "MSXCPU.hh" |
| 5 |
#include "MSXException.hh" |
| 6 |
#include "MSXMotherBoard.hh" |
| 7 |
#include "RealTime.hh" |
| 8 |
#include "Timer.hh" |
| 9 |
#include "serialize.hh" |
| 10 |
|
| 11 |
#include <algorithm> |
| 12 |
#include <array> |
| 13 |
#include <cassert> |
| 14 |
#include <chrono> |
| 15 |
|
| 16 |
namespace openmsx { |
| 17 |
|
| 18 |
|
| 19 |
static constexpr byte OP_OFFER = 0x01; |
| 20 |
static constexpr byte OP_HOLD = 0x02; |
| 21 |
static constexpr byte OP_CANCEL = 0x03; |
| 22 |
static constexpr byte OP_HELLO = 0x7E; |
| 23 |
static constexpr byte PROTOCOL_VERSION = 1; |
| 24 |
|
| 25 |
static constexpr byte MSXPI_VERSION = 0x0E; |
| 26 |
static constexpr int DEFAULT_SERVER_PORT = 5000; |
| 27 |
|
| 28 |
|
| 29 |
|
| 30 |
static constexpr auto TRANSFER_TIME = EmuDuration::usec(20); |
| 31 |
|
| 32 |
MSXPiDevice::MSXPiDevice(DeviceConfig& config) |
| 33 |
: MSXDevice(config) |
| 34 |
, activePort(DEFAULT_SERVER_PORT) |
| 35 |
, rom(getName() + " ROM", "MSXPi EEPROM", config) |
| 36 |
, bankSetting(getCommandController(), "msxpirom_bank", |
| 37 |
"MSXPi bank jumper, as labelled on the PCB: BANK2 (EEPROM A14 = CPU " |
| 38 |
"A15) puts the lower half (MSX-DOS) at 4000h, BANK1 (EEPROM A14 = CPU " |
| 39 |
"A14) the upper half (BIOS only). Takes effect at the next reset", |
| 40 |
Bank::A15, |
| 41 |
EnumSetting<Bank>::Map{{"BANK1", Bank::A14}, {"BANK2", Bank::A15}}) |
| 42 |
, sltslSetting(getCommandController(), "msxpirom_sltsl", |
| 43 |
"MSXPi J3 SLTSL jumper: OFF disconnects the EEPROM, the interface " |
| 44 |
"ports keep working. Takes effect at the next reset", Sltsl::ON, |
| 45 |
EnumSetting<Sltsl>::Map{{"ON", Sltsl::ON}, {"OFF", Sltsl::OFF}}) |
| 46 |
, portSetting(getCommandController(), "msxpiserver_port", |
| 47 |
"TCP port used to connect to the MSXPi server", |
| 48 |
DEFAULT_SERVER_PORT, 1, 65535) |
| 49 |
{ |
| 50 |
activePort = portSetting.getInt(); |
| 51 |
portSetting.attach(*this); |
| 52 |
if (rom.size() != 0x4000 && rom.size() != 0x8000) { |
| 53 |
throw MSXException("MSXPi EEPROM image must be 16KB or 32KB, got ", |
| 54 |
rom.size(), " bytes"); |
| 55 |
} |
| 56 |
bank = bankSetting.getEnum(); |
| 57 |
romEnabled = sltslSetting.getEnum() == Sltsl::ON; |
| 58 |
thread = std::thread(&MSXPiDevice::readLoop, this); |
| 59 |
} |
| 60 |
|
| 61 |
MSXPiDevice::~MSXPiDevice() |
| 62 |
{ |
| 63 |
portSetting.detach(*this); |
| 64 |
shouldStop = true; |
| 65 |
closeSocket(); |
| 66 |
if (thread.joinable()) { |
| 67 |
poller.abort(); |
| 68 |
thread.join(); |
| 69 |
} |
| 70 |
} |
| 71 |
|
| 72 |
void MSXPiDevice::update(const Setting& setting) noexcept |
| 73 |
{ |
| 74 |
(void)setting; |
| 75 |
assert(&setting == &portSetting); |
| 76 |
activePort = portSetting.getInt(); |
| 77 |
closeSocket(); |
| 78 |
} |
| 79 |
|
| 80 |
void MSXPiDevice::closeSocket() |
| 81 |
{ |
| 82 |
std::lock_guard lock(socketMtx); |
| 83 |
auto oldSock = sock.exchange(OPENMSX_INVALID_SOCKET); |
| 84 |
if (oldSock != OPENMSX_INVALID_SOCKET) { |
| 85 |
sock_close(oldSock); |
| 86 |
} |
| 87 |
} |
| 88 |
|
| 89 |
|
| 90 |
|
| 91 |
|
| 92 |
|
| 93 |
void MSXPiDevice::interfaceReset() |
| 94 |
{ |
| 95 |
|
| 96 |
|
| 97 |
|
| 98 |
busy = false; |
| 99 |
bound.reset(); |
| 100 |
srValue = 0x00; |
| 101 |
waitMode = false; |
| 102 |
} |
| 103 |
|
| 104 |
void MSXPiDevice::powerUp(EmuTime ) |
| 105 |
{ |
| 106 |
interfaceReset(); |
| 107 |
latch = 0xFF; |
| 108 |
applyJumpers(); |
| 109 |
} |
| 110 |
|
| 111 |
void MSXPiDevice::reset(EmuTime ) |
| 112 |
{ |
| 113 |
|
| 114 |
|
| 115 |
applyJumpers(); |
| 116 |
} |
| 117 |
|
| 118 |
|
| 119 |
|
| 120 |
|
| 121 |
|
| 122 |
void MSXPiDevice::applyJumpers() |
| 123 |
{ |
| 124 |
auto newBank = bankSetting.getEnum(); |
| 125 |
auto newEnabled = sltslSetting.getEnum() == Sltsl::ON; |
| 126 |
if (newBank == bank && newEnabled == romEnabled) return; |
| 127 |
bank = newBank; |
| 128 |
romEnabled = newEnabled; |
| 129 |
invalidateDeviceRCache(); |
| 130 |
} |
| 131 |
|
| 132 |
const byte* MSXPiDevice::romByte(uint16_t address) const |
| 133 |
{ |
| 134 |
|
| 135 |
if (!romEnabled) return unmappedRead.data(); |
| 136 |
unsigned a14 = (bank == Bank::A15) ? (address >> 15) & 1 : (address >> 14) & 1; |
| 137 |
unsigned chip = (a14 << 14) | (address & 0x3FFF); |
| 138 |
if (chip >= rom.size()) return unmappedRead.data(); |
| 139 |
return &rom[chip]; |
| 140 |
} |
| 141 |
|
| 142 |
byte MSXPiDevice::readMem(uint16_t address, EmuTime time) |
| 143 |
{ |
| 144 |
return peekMem(address, time); |
| 145 |
} |
| 146 |
|
| 147 |
byte MSXPiDevice::peekMem(uint16_t address, EmuTime ) const |
| 148 |
{ |
| 149 |
return *romByte(address); |
| 150 |
} |
| 151 |
|
| 152 |
const byte* MSXPiDevice::getReadCacheLine(uint16_t start) const |
| 153 |
{ |
| 154 |
|
| 155 |
return romByte(start); |
| 156 |
} |
| 157 |
|
| 158 |
bool MSXPiDevice::ready() const |
| 159 |
{ |
| 160 |
|
| 161 |
if (link != Link::FRAMED) return false; |
| 162 |
return bound || offerCount != 0; |
| 163 |
} |
| 164 |
|
| 165 |
void MSXPiDevice::startTransfer(EmuTime time) |
| 166 |
{ |
| 167 |
busy = true; |
| 168 |
bound.reset(); |
| 169 |
tryBind(time); |
| 170 |
} |
| 171 |
|
| 172 |
void MSXPiDevice::tryBind(EmuTime time) |
| 173 |
{ |
| 174 |
|
| 175 |
if (!busy || bound || link != Link::FRAMED || offerCount == 0) return; |
| 176 |
std::lock_guard lock(mtx); |
| 177 |
if (offers.empty()) return; |
| 178 |
bound = offers.front(); |
| 179 |
offers.pop_front(); |
| 180 |
offerCount = offers.size(); |
| 181 |
boundEpoch = epoch; |
| 182 |
|
| 183 |
startTime = time; |
| 184 |
doneTime = time + TRANSFER_TIME; |
| 185 |
sendFrame(OP_OFFER, latch); |
| 186 |
} |
| 187 |
|
| 188 |
void MSXPiDevice::complete() |
| 189 |
{ |
| 190 |
srValue = bound->miso; |
| 191 |
bound.reset(); |
| 192 |
busy = false; |
| 193 |
} |
| 194 |
|
| 195 |
void MSXPiDevice::update(EmuTime time) |
| 196 |
{ |
| 197 |
if (bound && boundEpoch != epoch) { |
| 198 |
|
| 199 |
|
| 200 |
|
| 201 |
bound.reset(); |
| 202 |
} |
| 203 |
if (busy && !bound) tryBind(time); |
| 204 |
if (bound && time >= doneTime) complete(); |
| 205 |
} |
| 206 |
|
| 207 |
byte MSXPiDevice::shiftRegister(EmuTime time) const |
| 208 |
{ |
| 209 |
if (!busy) return srValue; |
| 210 |
|
| 211 |
|
| 212 |
if (!bound || time <= startTime) return 0x01; |
| 213 |
auto elapsed = (time - startTime).toUint64(); |
| 214 |
auto total = std::max<uint64_t>(TRANSFER_TIME.toUint64(), 1); |
| 215 |
auto bits = unsigned(std::min<uint64_t>(8, (elapsed * 9) / total)); |
| 216 |
unsigned sr = (1u << bits) | (unsigned(bound->miso) >> (8 - bits)); |
| 217 |
return byte(sr); |
| 218 |
} |
| 219 |
|
| 220 |
void MSXPiDevice::waitForPeer(EmuTime time) |
| 221 |
{ |
| 222 |
|
| 223 |
|
| 224 |
|
| 225 |
|
| 226 |
|
| 227 |
|
| 228 |
|
| 229 |
static constexpr uint64_t STEP_US = 1000; |
| 230 |
static constexpr uint64_t ALLOWED_LAG_US = 20000; |
| 231 |
if (link != Link::FRAMED) return; |
| 232 |
auto& realTime = getMotherBoard().getRealTime(); |
| 233 |
std::unique_lock lock(mtx); |
| 234 |
while (offers.empty() && link == Link::FRAMED && |
| 235 |
realTime.timeLeft(STEP_US + ALLOWED_LAG_US, time)) { |
| 236 |
offerCv.wait_for(lock, std::chrono::microseconds(STEP_US)); |
| 237 |
} |
| 238 |
} |
| 239 |
|
| 240 |
EmuTime MSXPiDevice::stall(EmuTime time) |
| 241 |
{ |
| 242 |
|
| 243 |
|
| 244 |
|
| 245 |
if (!waitMode || !busy || !bound) return time; |
| 246 |
if (doneTime > time) { |
| 247 |
getCPU().wait(doneTime); |
| 248 |
update(doneTime); |
| 249 |
return doneTime; |
| 250 |
} |
| 251 |
return time; |
| 252 |
} |
| 253 |
|
| 254 |
byte MSXPiDevice::readIO(uint16_t port, EmuTime time) |
| 255 |
{ |
| 256 |
switch (port & 0xff) { |
| 257 |
case 0x56: |
| 258 |
update(time); |
| 259 |
if (!bound && offerCount == 0) { |
| 260 |
|
| 261 |
waitForPeer(time); |
| 262 |
update(time); |
| 263 |
} |
| 264 |
return peekIO(port, time); |
| 265 |
case 0x57: |
| 266 |
update(time); |
| 267 |
return peekIO(port, time); |
| 268 |
case 0x5A: |
| 269 |
update(time); |
| 270 |
if (waitMode) { |
| 271 |
|
| 272 |
if (!busy && ready()) startTransfer(time); |
| 273 |
return shiftRegister(stall(time)); |
| 274 |
} |
| 275 |
return shiftRegister(time); |
| 276 |
default: |
| 277 |
return 0xFF; |
| 278 |
} |
| 279 |
} |
| 280 |
|
| 281 |
byte MSXPiDevice::peekIO(uint16_t port, EmuTime time) const |
| 282 |
{ |
| 283 |
switch (port & 0xff) { |
| 284 |
case 0x56: |
| 285 |
return (busy || !ready()) ? 0x01 : 0x00; |
| 286 |
case 0x57: |
| 287 |
return byte((waitMode ? 0x80 : 0x00) | MSXPI_VERSION); |
| 288 |
case 0x5A: |
| 289 |
return shiftRegister(time); |
| 290 |
default: |
| 291 |
return 0xFF; |
| 292 |
} |
| 293 |
} |
| 294 |
|
| 295 |
void MSXPiDevice::writeIO(uint16_t port, byte value, EmuTime time) |
| 296 |
{ |
| 297 |
switch (port & 0xff) { |
| 298 |
case 0x56: |
| 299 |
if (value == 0xFF) { |
| 300 |
latch = value; |
| 301 |
interfaceReset(); |
| 302 |
break; |
| 303 |
} |
| 304 |
[[fallthrough]]; |
| 305 |
case 0x5A: |
| 306 |
update(time); |
| 307 |
latch = value; |
| 308 |
|
| 309 |
if (!busy) startTransfer(time); |
| 310 |
stall(time); |
| 311 |
break; |
| 312 |
case 0x57: |
| 313 |
|
| 314 |
if (value == 0x01) { |
| 315 |
waitMode = true; |
| 316 |
} else if (value == 0x00) { |
| 317 |
waitMode = false; |
| 318 |
} |
| 319 |
break; |
| 320 |
default: |
| 321 |
break; |
| 322 |
} |
| 323 |
} |
| 324 |
|
| 325 |
|
| 326 |
|
| 327 |
|
| 328 |
|
| 329 |
void MSXPiDevice::sendFrame(byte op, byte arg) |
| 330 |
{ |
| 331 |
auto s = sock.load(); |
| 332 |
if (s == OPENMSX_INVALID_SOCKET) return; |
| 333 |
std::array<char, 2> frame = {char(op), char(arg)}; |
| 334 |
(void)sock_send(s, frame.data(), frame.size()); |
| 335 |
} |
| 336 |
|
| 337 |
void MSXPiDevice::handleFrame(byte op, byte arg) |
| 338 |
{ |
| 339 |
switch (op) { |
| 340 |
case OP_HOLD: |
| 341 |
pendingHold.emplace_back(arg, true); |
| 342 |
break; |
| 343 |
case OP_OFFER: { |
| 344 |
|
| 345 |
std::lock_guard lock(mtx); |
| 346 |
offers.insert(offers.end(), pendingHold.begin(), pendingHold.end()); |
| 347 |
offers.emplace_back(arg, false); |
| 348 |
offerCount = offers.size(); |
| 349 |
pendingHold.clear(); |
| 350 |
offerCv.notify_all(); |
| 351 |
break; |
| 352 |
} |
| 353 |
case OP_CANCEL: { |
| 354 |
|
| 355 |
|
| 356 |
|
| 357 |
std::lock_guard lock(mtx); |
| 358 |
pendingHold.clear(); |
| 359 |
offers.clear(); |
| 360 |
offerCount = 0; |
| 361 |
sendFrame(OP_CANCEL, 0); |
| 362 |
break; |
| 363 |
} |
| 364 |
default: |
| 365 |
break; |
| 366 |
} |
| 367 |
} |
| 368 |
|
| 369 |
bool MSXPiDevice::connectSocket() |
| 370 |
{ |
| 371 |
SOCKET s = socket(AF_INET, SOCK_STREAM, 0); |
| 372 |
if (s == OPENMSX_INVALID_SOCKET) return false; |
| 373 |
auto port = activePort.load(); |
| 374 |
auto addr = sock_makeIPv4(INADDR_LOOPBACK, uint16_t(port)); |
| 375 |
|
| 376 |
|
| 377 |
sock_setNoDelay(s); |
| 378 |
if (connect(s, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0) { |
| 379 |
sock_close(s); |
| 380 |
return false; |
| 381 |
} |
| 382 |
|
| 383 |
std::lock_guard lock(socketMtx); |
| 384 |
if ((port != activePort.load()) || (sock != OPENMSX_INVALID_SOCKET)) { |
| 385 |
sock_close(s); |
| 386 |
return false; |
| 387 |
} |
| 388 |
sock = s; |
| 389 |
return true; |
| 390 |
} |
| 391 |
|
| 392 |
void MSXPiDevice::readLoop() |
| 393 |
{ |
| 394 |
std::array<char, 4096> buf; |
| 395 |
while (!shouldStop) { |
| 396 |
if (sock == OPENMSX_INVALID_SOCKET) { |
| 397 |
link = Link::DOWN; |
| 398 |
if (!connectSocket()) { |
| 399 |
Timer::sleep(1'000'000); |
| 400 |
continue; |
| 401 |
} |
| 402 |
{ |
| 403 |
std::lock_guard lock(mtx); |
| 404 |
offers.clear(); |
| 405 |
offerCount = 0; |
| 406 |
} |
| 407 |
pendingHold.clear(); |
| 408 |
partial.reset(); |
| 409 |
++epoch; |
| 410 |
link = Link::PROBING; |
| 411 |
} |
| 412 |
|
| 413 |
#ifndef _WIN32 |
| 414 |
if (poller.poll(sock)) { |
| 415 |
continue; |
| 416 |
} |
| 417 |
#endif |
| 418 |
auto n = sock_recv(sock, buf.data(), buf.size()); |
| 419 |
if (n <= 0) { |
| 420 |
closeSocket(); |
| 421 |
continue; |
| 422 |
} |
| 423 |
|
| 424 |
size_t i = 0; |
| 425 |
if (link == Link::PROBING) { |
| 426 |
if (!partial) { |
| 427 |
if (byte(buf[0]) != OP_HELLO) { |
| 428 |
|
| 429 |
|
| 430 |
link = Link::INCOMPATIBLE; |
| 431 |
} else { |
| 432 |
partial = byte(buf[i++]); |
| 433 |
} |
| 434 |
} |
| 435 |
if (partial && i < size_t(n)) { |
| 436 |
partial.reset(); |
| 437 |
++i; |
| 438 |
std::lock_guard lock(mtx); |
| 439 |
sendFrame(OP_HELLO, PROTOCOL_VERSION); |
| 440 |
link = Link::FRAMED; |
| 441 |
} |
| 442 |
} |
| 443 |
if (link == Link::FRAMED) { |
| 444 |
for (; i < size_t(n); ++i) { |
| 445 |
auto b = byte(buf[i]); |
| 446 |
if (!partial) { |
| 447 |
partial = b; |
| 448 |
} else { |
| 449 |
handleFrame(*partial, b); |
| 450 |
partial.reset(); |
| 451 |
} |
| 452 |
} |
| 453 |
} |
| 454 |
} |
| 455 |
} |
| 456 |
|
| 457 |
|
| 458 |
|
| 459 |
|
| 460 |
|
| 461 |
|
| 462 |
|
| 463 |
template<typename Archive> |
| 464 |
void MSXPiDevice::serialize(Archive& ar, unsigned ) |
| 465 |
{ |
| 466 |
ar.template serializeBase<MSXDevice>(*this); |
| 467 |
|
| 468 |
auto bankA14 = byte(bank == Bank::A14); |
| 469 |
ar.serialize("bankA14", bankA14, |
| 470 |
"romEnabled", romEnabled, |
| 471 |
"latch", latch, |
| 472 |
"busy", busy, |
| 473 |
"waitMode", waitMode, |
| 474 |
"srValue", srValue); |
| 475 |
|
| 476 |
if constexpr (Archive::IS_LOADER) { |
| 477 |
bank = bankA14 ? Bank::A14 : Bank::A15; |
| 478 |
bound.reset(); |
| 479 |
invalidateDeviceRCache(); |
| 480 |
} |
| 481 |
} |
| 482 |
INSTANTIATE_SERIALIZE_METHODS(MSXPiDevice); |
| 483 |
REGISTER_MSXDEVICE(MSXPiDevice, "MSXPiDevice"); |
| 484 |
|
| 485 |
} |