Commits

spencercw committed 130d5d0

#33 Implement serial comms over the network.

This still has issues where data is randomly lost. Perhaps because the emulators get out of sync.

Comments (0)

Files changed (6)

gb_emulator/include/gb_emulator/constants.hpp

  * inaccessible during transfers so emulating the bit shifting is unnecessary. */
 const int SERIAL1_CYCLES                = 1024;  /* 1024 Hz (8192 Hz) */
 const int SERIAL2_CYCLES                = 32;    /* 32 768 Hz (262 144 Hz) */
+/* Maximum number of cycles to execute when an internal clock request is received when the game is
+ * not ready for it before giving up. (~8ms) */
+const int SERIAL_DELAY_ALLOWANCE        = 8192;
 
 #endif

gb_emulator/include/gb_emulator/gb.hpp

 	boost::scoped_ptr<GbVideo> video_;
 	boost::scoped_ptr<GbMemory> mem_;
 	boost::scoped_ptr<GbCommsSerial> serial_;
+	int serialCycles_;
 	GbDebugger debugger_;
 	bool gbc_;
 

gb_emulator/include/gb_emulator/gb_comms_serial.h

 #include <string>
 
 #include <boost/asio/ip/tcp.hpp>
+#include <boost/optional.hpp>
 #include <boost/shared_ptr.hpp>
 #include <boost/signals/connection.hpp>
+#include <boost/logic/tribool.hpp>
 
 #include <gb_net/types.h>
 
 	/**
 	 * No remote connections are possible when using this constructor.
 	 * \param gb The emulator context.
+	 * \param cycles Parameter which may be set at any time to indicate when poll() should be
+	 * called. \c numeric_limits<int>::max() is used as a sentinel value to mean poll() should not
+	 * be called. This value should be decremented every time a CPU cycle is executed and when it
+	 * reaches zero poll() should be called. Reset the value to numeric_limits<int>::max() before
+	 * calling poll().
 	 */
-	GbCommsSerial(Gb &gb);
+	GbCommsSerial(Gb &gb, int &cycles);
 
 	//! Constructor; starts a serial server on the given TCP port.
 	/**
 	 * \param gb The emulator context.
+	 * \param cycles See GbCommsSerial(Gb &, int &) for details on this parameter.
 	 * \param ioService The I/O service to use for communications.
 	 * \param port The TCP port to listen from remote connections on.
 	 */
-	GbCommsSerial(Gb &gb, boost::asio::io_service &ioService, uint16_t port);
+	GbCommsSerial(Gb &gb, int &cycles, boost::asio::io_service &ioService, uint16_t port);
 
 	//! Constructor; starts a serial client and connections to the given server specification.
 	/**
 	 * \param gb The emulator context.
+	 * \param cycles See GbCommsSerial(Gb &, int &) for details on this parameter.
 	 * \param ioService The I/O service to use for communications.
 	 * \param host The hostname or IP address (may be IPv4 or IPv6 where the operating system
 	 * supports it) of the remote server.
 	 * \param port The TCP port the server is listening on.
 	 */
-	GbCommsSerial(Gb &gb, boost::asio::io_service &ioService, const std::string &host,
+	GbCommsSerial(Gb &gb, int &cycles, boost::asio::io_service &ioService, const std::string &host,
 		uint16_t port);
 
-	//! Performs a single transfer.
+	//! Handles events that occur after a period of time.
 	/**
-	 * This should be called when bit 7 of the SC register is set to one and cycle counter for this
-	 * class has reached zero.
-	 * \return The number of CPU cycles to execute before calling this function again. If zero is
-	 * returned then this should not be called until a transfer is next initiated.
+	 * This should be called when the \c cycles parameter passed to the constructor reaches zero.
 	 */
-	int poll();
+	void poll();
+
+	//! Handles a write to a serial comms register.
+	/**
+	 * \param ptr The lower eight bits of the register address (the upper eight bits are always
+	 * 0xff).
+	 */
+	void writeIoPort(uint8_t ptr, uint8_t val);
 
 private:
+
+#pragma pack(push)
+#pragma pack(1)
+
+	// Message structure used for network communications
+	struct SerialDataMessage
+	{
+		// Message identifiers
+		enum Type
+		{
+			// Internal clock message. The remote host is expected to reply with a TYPE_EXT or
+			// TYPE_EXT_INACTIVE message
+			TYPE_INT = 0xbe00,
+			TYPE_EXT,
+			// External clock message. This is sent in reply to a TYPE_INT message.
+			TYPE_DATA,
+			// External clock message. This is sent in reply to a TYPE_INT message when the local
+			// program is not requesting any transfer. The data member is unused in this kind of
+			// message.
+			TYPE_INACTIVE
+		};
+
+		// Message sequence number. This is used to match request and response messages
+		uint8_t sequence;
+		// Data value
+		uint8_t data;
+	};
+
+#pragma pack(pop)
+
 	// The emulator context
 	Gb &gb_;
 
+	// Parameter used to control when poll() is called
+	int &cycles_;
+
+	// Indicates whether we are connected to another emulator
+	bool connected_;
+
 	// Whether a transfer has been started but not completed
-	bool transferStarted_;
+	// indeterminate indicates an internal clock tra
+	boost::tribool transferStatus_;
+
+	// Copy of the received packet when an internal clock request is received before the game is
+	// ready for it
+	boost::optional<SerialDataMessage> intDelay_;
+
+	// Current line state. This is set to bit zero of the last transmitted byte. This is used to
+	// determine the value sent when an internal clock request is received but no external clock
+	// transfer is active.
+	bool lineState_;
+
+	// Whether the emulator has been paused. This is used when a reply for an internal clock request
+	// is not received in time. The emulator is paused until the response is received.
+	bool paused_;
 
 	// The networking interface if we are using it
 	boost::shared_ptr<gbnet::Networking> net_;
 
+	// Current sequence number for network messages
+	uint8_t sequence_;
+
+	// Data being transmitted in an external clock transmission
+	uint8_t data_;
+
 	// Connects the networking callbacks below to net_
 	void connectNetworkSignals();
 

gb_emulator/src/gb.cpp

 static const unsigned RAM_SIZES[] = { 0, 2048, 8192, 32768 };
 
 Gb::Gb(const GbConfig &config):
-	frames_(0),
-	cpu_(*this),
-	timers_(*this),
-	debugger_(*this, ioService_)
+frames_(0),
+cpu_(*this),
+timers_(*this),
+debugger_(*this, ioService_),
+serialCycles_(numeric_limits<int>::max())
 {
 	gbc_ = true;
 
 	if (!config.serialHost.empty() && config.serialPort)
 	{
 		// Client
-		serial_.reset(new GbCommsSerial(*this, ioService_, config.serialHost, config.serialPort));
+		serial_.reset(new GbCommsSerial(*this, serialCycles_, ioService_, config.serialHost,
+			config.serialPort));
 	}
 	else if (config.serialPort)
 	{
 		// Server
-		serial_.reset(new GbCommsSerial(*this, ioService_, config.serialPort));
+		serial_.reset(new GbCommsSerial(*this, serialCycles_, ioService_, config.serialPort));
 	}
 	else
 	{
 		// No connectivity requested
-		serial_.reset(new GbCommsSerial(*this));
+		serial_.reset(new GbCommsSerial(*this, serialCycles_));
 	}
 
 	// Load the BIOS
 	int videoCycles = HBLANK_CYCLES;
 	int timerCycles = DIVIDER_CYCLES;
 	double soundCycles = 0;
-	int serialCycles = 0;
 	int frameCycles = CYCLES_PER_FRAME;
 	for (;;)
 	{
 		}
 
 		// Update the serial comms
-		if ((mem_->ioPorts[SC] & 0x80) && serialCycles <= 0)
+		if (serialCycles_ <= 0)
 		{
-			int tmpCycles = serial_->poll();
-			if (tmpCycles)
-			{
-				serialCycles += tmpCycles;
-			}
-			else
-			{
-				serialCycles = 0;
-			}
+			serialCycles_ = numeric_limits<int>::max();
+			serial_->poll();
 		}
 
 		// Wait for the sound timer to signal we are ok to continue emulation
 		if (timerCycles < cycles) cycles = timerCycles;
 		if (soundCycles < cycles) cycles = soundCycles;
 		if (frameCycles < cycles) cycles = frameCycles;
-		if ((mem_->ioPorts[SC] & 0x80) && serialCycles < cycles) cycles = serialCycles;
+		if (serialCycles_ < cycles) cycles = serialCycles_;
 
 		bool speedSwitch = false;
 		int cyclesExecuted = cpu_.poll(static_cast<int>(ceil(cycles)), speedSwitch);
 		videoCycles -= cyclesExecuted;
 		soundCycles -= cyclesExecuted;
 		frameCycles -= cyclesExecuted;
-		if (mem_->ioPorts[SC] & 0x80 && serialCycles > 0)
+		if (serialCycles_ != numeric_limits<int>::max())
 		{
-			serialCycles -= cyclesExecuted;
+			serialCycles_ -= cyclesExecuted;
 		}
 
 		// Adjust the cycle counts if the CPU speed has switched

gb_emulator/src/gb_comms_serial.cpp

 
 #include <assert.h>
 
+#include <iomanip>
 #include <iostream>
+#include <limits>
 
 #include <gb_net/tcp_client.h>
 #include <gb_net/tcp_server.h>
 namespace io = boost::asio;
 using namespace gbnet;
 using boost::asio::ip::tcp;
+using boost::indeterminate;
 using std::basic_string;
 using std::clog;
+using std::hex;
+using std::numeric_limits;
+using std::setw;
+using std::setfill;
 using std::string;
 
-GbCommsSerial::GbCommsSerial(Gb &gb):
+GbCommsSerial::GbCommsSerial(Gb &gb, int &cycles):
 gb_(gb),
-transferStarted_(false)
+cycles_(cycles),
+connected_(false),
+transferStatus_(false),
+lineState_(true),
+paused_(false),
+sequence_(0)
 {
 }
 
-GbCommsSerial::GbCommsSerial(Gb &gb, io::io_service &ioService, uint16_t port):
+GbCommsSerial::GbCommsSerial(Gb &gb, int &cycles, io::io_service &ioService, uint16_t port):
 gb_(gb),
-transferStarted_(false),
-net_(new TcpServer(ioService, port))
+cycles_(cycles),
+connected_(false),
+transferStatus_(false),
+lineState_(true),
+paused_(false),
+net_(new TcpServer(ioService, port)),
+sequence_(0)
 {
 	connectNetworkSignals();
 }
 
-GbCommsSerial::GbCommsSerial(Gb &gb, io::io_service &ioService, const string &host, uint16_t port):
+GbCommsSerial::GbCommsSerial(Gb &gb, int &cycles, io::io_service &ioService, const string &host,
+	uint16_t port):
 gb_(gb),
-transferStarted_(false),
-net_(new TcpClient(ioService, host, port))
+cycles_(cycles),
+connected_(false),
+transferStatus_(false),
+lineState_(true),
+paused_(false),
+net_(new TcpClient(ioService, host, port)),
+sequence_(0),
+data_(0)
 {
 	connectNetworkSignals();
 }
 
-int GbCommsSerial::poll()
+void GbCommsSerial::poll()
 {
-	assert(gb_.mem_->ioPorts[SC] & 0x80);
+	assert(gb_.mem_->ioPorts[SC] & 0x80 || intDelay_);
 
-	if (!transferStarted_)
+	if (transferStatus_)
 	{
-		transferStarted_ = true;
+		// Mark the transfer as completed
+		transferStatus_ = false;
+		gb_.mem_->ioPorts[SC] &= ~0x80;
+		gb_.mem_->ioPorts[IF] |= SERIAL_INTR;
+	}
+	else if (!transferStatus_ && intDelay_)
+	{
+		// An internal clock request was received but the program is still not ready for it so give
+		// up and send back a negative response
+		SerialDataMessage response;
+		response.sequence = intDelay_->sequence;
+		response.data = lineState_ ? 0xff : 0x00;
 
-		// Return the duration of the transfer. If the external clock is used just assumed it is the
-		// standard 8192 Hz clock.
-		if (gb_.gbc_ && (gb_.mem_->ioPorts[SC] & 0x03) == 0x03)
+		intDelay_.reset();
+		net_->postMessage(SerialDataMessage::TYPE_INACTIVE,
+			basic_string<uint8_t>(reinterpret_cast<uint8_t *>(&response), sizeof(response)));
+	}
+	else if (indeterminate(transferStatus_))
+	{
+		// An internal clock request has been sent but no response has yet been received; wait until
+		// it arrives
+		paused_ = true;
+		gb_.cpu_.pause = true;
+	}
+}
+
+void GbCommsSerial::writeIoPort(uint8_t ptr, uint8_t val)
+{
+	switch (ptr)
+	{
+	case SB:
+		gb_.mem_->ioPorts[ptr] = val;
+		break;
+
+	case SC:
+		// Bit 1 is not accessible in DMG mode
+		if (gb_.gbc_)
 		{
-			return SERIAL2_CYCLES;
+			gb_.mem_->ioPorts[ptr] = val & 0x83;
 		}
 		else
 		{
-			return SERIAL1_CYCLES;
+			gb_.mem_->ioPorts[ptr] = val & 0x81;
 		}
-	}
-	else
-	{
-		// Mark the transfer as completed. Fill SB with 0xff to indicate there is no connection
-		transferStarted_ = false;
-		gb_.mem_->ioPorts[SB]  = 0xff;
-		gb_.mem_->ioPorts[SC] &= ~0x80;
-		return 0;
+
+		// Start the transfer if necessary
+		if (val & 0x80)
+		{
+			// Send the request to the remote host if we are connected
+			if (connected_)
+			{
+				if (gb_.mem_->ioPorts[SC] & 0x01)
+				{
+					// Internal clock
+					transferStatus_ = indeterminate;
+					intDelay_.reset();
+
+					SerialDataMessage message;
+					message.sequence = ++sequence_;
+					message.data = gb_.mem_->ioPorts[SB];
+					lineState_ = message.data & 0x01;
+
+					net_->postMessage(SerialDataMessage::TYPE_INT,
+						basic_string<uint8_t>(reinterpret_cast<uint8_t *>(&message), sizeof(message)));
+				}
+				else
+				{
+					// External clock
+					if (intDelay_)
+					{
+						// An internal clock request was received before we were ready for it; send
+						// the response now
+						SerialDataMessage response;
+						response.sequence = intDelay_->sequence;
+						response.data = gb_.mem_->ioPorts[SB];
+						lineState_ = response.data & 0x01;
+
+						// Save the received value
+						transferStatus_ = false;
+						gb_.mem_->ioPorts[SB]  = intDelay_->data;
+						gb_.mem_->ioPorts[SC] &= ~0x80;
+						gb_.mem_->ioPorts[IF] |= SERIAL_INTR;
+
+						net_->postMessage(SerialDataMessage::TYPE_DATA,
+							basic_string<uint8_t>(reinterpret_cast<uint8_t *>(&response), sizeof(response)));
+
+						intDelay_.reset();
+						cycles_ = numeric_limits<int>::max();
+					}
+					else
+					{
+						transferStatus_ = indeterminate;
+						data_ = gb_.mem_->ioPorts[SB];
+					}
+				}
+			}
+			else
+			{
+				// Fill SB with 0xff to indicate there is no connection
+				transferStatus_ = true;
+				intDelay_.reset();
+				gb_.mem_->ioPorts[SB] = 0xff;
+			}
+
+			// If this is an internal clock transfer we need to be called back after one transfer
+			// period
+			if (gb_.mem_->ioPorts[SC] & 0x01)
+			{
+				if (gb_.gbc_ && (gb_.mem_->ioPorts[SC] & 0x02))
+				{
+					cycles_ += SERIAL2_CYCLES;
+				}
+				else
+				{
+					cycles_ += SERIAL1_CYCLES;
+				}
+			}
+		}
+
+		break;
 	}
 }
 
 
 void GbCommsSerial::onConnected(const tcp::endpoint &endpoint)
 {
+	connected_ = true;
 	clog << "Connection established to " << endpoint << "\n";
 }
 
 void GbCommsSerial::onMessageReceived(uint16_t identifier, const basic_string<uint8_t> &message)
 {
-	(void) identifier;
-	(void) message;
-	clog << "Message received\n";
+	if (message.size() != sizeof(SerialDataMessage))
+	{
+		clog << "Data message received with incorrect length\n";
+		return;
+	}
+	const SerialDataMessage *data = reinterpret_cast<const SerialDataMessage *>(message.data());
+
+	switch (identifier)
+	{
+	case SerialDataMessage::TYPE_INT:
+		if (!transferStatus_ || (gb_.mem_->ioPorts[SC] & 0x01))
+		{
+			// No transfer is active, the active transfer has already completed or we are also
+			// trying to send data using the internal clock. Allow a brief period for the program to
+			// request an external clock transfer before sending a negative response
+			intDelay_ = *data;
+			cycles_ = SERIAL_DELAY_ALLOWANCE;
+		}
+		else
+		{
+			// Send our data back
+			SerialDataMessage response;
+			response.sequence = data->sequence;
+			response.data = data_;
+			lineState_ = data_ & 0x01;
+
+			// Save the received value
+			transferStatus_ = false;
+			intDelay_.reset();
+			gb_.mem_->ioPorts[SB]  = data->data;
+			gb_.mem_->ioPorts[SC] &= ~0x80;
+			gb_.mem_->ioPorts[IF] |= SERIAL_INTR;
+
+			net_->postMessage(SerialDataMessage::TYPE_DATA,
+				basic_string<uint8_t>(reinterpret_cast<uint8_t *>(&response), sizeof(response)));
+		}
+		break;
+
+	case SerialDataMessage::TYPE_DATA:
+	case SerialDataMessage::TYPE_INACTIVE:
+		// Save the received value if a transfer is in progress
+		if (data->sequence == sequence_ && indeterminate(transferStatus_))
+		{
+			gb_.mem_->ioPorts[SB] = data->data;
+			if (paused_)
+			{
+				// The emulator has been suspended waiting for the data; resume operation now
+				transferStatus_ = false;
+				paused_ = false;
+				gb_.cpu_.pause = false;
+				gb_.mem_->ioPorts[SC] &= ~0x80;
+				gb_.mem_->ioPorts[IF] |= SERIAL_INTR;
+			}
+			else
+			{
+				// Mark the transfer as complete
+				transferStatus_ = true;
+			}
+		}
+		break;
+
+	default:
+		clog << hex << setfill('0') << "Unknown message with identifier 0x"
+		     << setw(4) << static_cast<unsigned>(identifier) << " received\n";
+	}
 }
 
 void GbCommsSerial::onBadPacket()
 {
+	connected_ = false;
 	clog << "A badly formed packet was received\n";
 }
 
 void GbCommsSerial::onNetworkError(ErrorType type, const bs::error_code &ec)
 {
+	connected_ = false;
 	switch (type)
 	{
 	case GBNET_ERR_RESOLVE:

gb_emulator/src/gb_memory.cpp

 {
 	switch (ptr)
 	{
-	// Serial transfer data
-	case SB:
-		// Access is only permitted when a transfer is not in progress
-		if (ioPorts[SC] & 0x80)
-		{
-			return 0;
-		}
-		return ioPorts[ptr];
-
 	// Sound modes
 	case NR14:
 	case NR24:
 		gb_.input_->translate();
 		break;
 
-	// Serial transfer data
+	// Serial transfer
 	case SB:
-		// Access is only permitted when a transfer is not in progress
-		if (!(ioPorts[SC] & 0x80))
-		{
-			ioPorts[ptr] = val;
-		}
-		break;
-
-	// Serial transfer control
 	case SC:
-		// Don't do anything if a transfer is in progress. The documentation doesn't say anything
-		// about this and it just makes the implementation simpler.
-		if (ioPorts[ptr] & 0x80)
-		{
-			return;
-		}
-
-		// Bit 1 is not accessible in DMG mode
-		if (gb_.gbc_)
-		{
-			ioPorts[ptr] = val & 0x83;
-		}
-		else
-		{
-			ioPorts[ptr] = val & 0x81;
-		}
+		gb_.serial_->writeIoPort(ptr, val);
 		break;
 
 	// Divider