// license:BSD-3-Clause // copyright-holders:Vas Crabb /*********************************************************************** High-level simulation of communication using the TOURNAMENT board. (Thanks to Darksoft for tracing out the circuit board, and Gregory Lewandowski for writing a proof of concept implementation.) This board has two sections, TOURNAMENT and REGISTER, but has only ever been seen with the TOURNAMENT section populated. There are two mini-DIN connectors: a black one labelled "IN" and grey one labelled "OUT". Each connector provides an RS-232 input and output, all of which are connected to a MAX232 providing level translation. Systems are daisy-chained, out to in, with a loopback connector on the IN port at the end of the chain. A NEC µPD71051C USART handles serialisation/framing. The game always selects asynchronous mode, x16 transmit/receive clock, 8 data bits, no parity, and one transmitted stop bit. The transmit/receive clock is generated by dividing down the 8 MHz clock with a pair of cascaded counters counters. A PAL16L8BCN is used for routing signals. It's purely combinatorial with the three control bits latched separately. It controls the transmit/receive clock, the output signals on the IN and OUT ports, and the RxD input to the USART. D4 controls the transmit/receive clock (halves the data rate when set). D1 ad D0 control how the serial data is routed. The equations are effectively: RxCLK/TxCLK <= D4 ? Qd : Qc RxD <= Tin1 <= !D0 ? 1 : D1 ? TxD : (TxD & Rout2) Tin2 <= (!D1 & D0) ? Rout1 : (D1 & !D0) ? TxD : 1 In tabular form: D1 D0 RxD Tin1 Tin2 0 0 1 1 1 0 1 Rout1 TxD & Rout2 Rout1 1 0 Rout2 1 TxD 1 1 Rout1 TxD 1 Mode 00 disables communication. Mode 01 connects the USART to the IN port, also forwarding signals between the IN and OUT ports. Mode 10 connects the USART to the OUT port and disconnects the IN port. Mode 11 connects the USART to the IN port and disconnects the OUT port. Mode 11 is used by the game while discovering its position in the daisy chain. Mode 10 is used during normal operation on the system at the head of the chain (with the loopback connector on its IN port). Mode 01 is used during normal operation on the remaining systems in the daisy chain. There's no flow control or collision detection in the upstream direction. It relies on the fact that the games only transmit briefly so collisions are statistically unlikely. To use this, enable communication in the Machine Configuration menu and use the communication settings to daisy-chain for systems. The system at the head of the daisy chain must have the comm_remotehost option set to an empty string. For example the four instances could be run as follows on a single machine - change the comm_remotehost options as necessary to connect multiple machines: mame ssf2tb -comm_localhost 0.0.0.0 -comm_localport 1234 -comm_remotehost "" mame ssf2tb -comm_localhost 0.0.0.0 -comm_localport 1235 -comm_remotehost 127.0.0.1 -comm_remoteport 1234 mame ssf2tb -comm_localhost 0.0.0.0 -comm_localport 1236 -comm_remotehost 127.0.0.1 -comm_remoteport 1235 mame ssf2tb -comm_localhost "" -comm_remotehost 127.0.0.1 -comm_remoteport 1236 TODO: * When the USART's RxRDY output is asserted, CPU-IPL2 will be asserted (via CPSB-OBJUP). The game doesn't seem to require this to work. ***********************************************************************/ #include "emu.h" #include "cps2comm.h" #include "emuopts.h" #include "asio.h" #include #include #include #include #include #include #include #include #include #include #include //#define VERBOSE 1 #include "logmacro.h" namespace { INPUT_PORTS_START(cps2comm) PORT_START("CFG") PORT_CONFNAME(0x01, 0x00, "Communication") PORT_CONFSETTING( 0x00, "Disable") PORT_CONFSETTING( 0x01, "Enable") INPUT_PORTS_END } // anonymous namespace class cps2_comm_device::context { public: context( cps2_comm_device &device, std::optional const &local, std::optional const &remote) : m_device(device), m_acceptor(m_ioctx), m_in_sock(m_ioctx), m_out_sock(m_ioctx), m_connect_timeout(m_ioctx), m_local(local), m_remote(remote), m_route(0U), m_stopping(false), m_in_connected(false), m_usart_wp(0U), m_usart_rp(0U) { } std::error_code start() { std::error_code err; if (m_remote) { m_in_sock.open(m_remote->protocol(), err); if (err) return err; } if (m_local) { m_acceptor.open(m_local->protocol(), err); if (!err) m_acceptor.bind(*m_local, err); if (!err) m_acceptor.listen(1, err); if (err) return err; } m_thread = std::thread( [this] () { if (m_local) start_accept(); if (m_remote) start_connect(); m_ioctx.run(); LOG("Network thread completed\n"); }); return err; } void stop() { m_ioctx.post( [this] () { m_stopping = true; std::error_code err; if (m_acceptor.is_open()) m_acceptor.close(err); if (m_in_sock.is_open()) m_in_sock.close(err); if (m_out_sock.is_open()) m_out_sock.close(err); m_connect_timeout.cancel(); }); m_thread.join(); } void set_routing(u8 val) { m_ioctx.post( [this, val] () { if (BIT(val, 0, 2) == 0U) { unsigned rp = m_usart_rp.load(std::memory_order_relaxed); m_usart_wp.store(rp, std::memory_order_release); } m_route = val; }); } bool usart_get(u8 &data) { unsigned const rp = m_usart_rp.load(std::memory_order_relaxed); unsigned const wp = m_usart_wp.load(std::memory_order_acquire); if (rp == wp) return false; data = m_usart_buf[rp]; m_usart_rp.store((rp + 1) % m_usart_buf.size(), std::memory_order_release); return true; } void send_in(u8 data) { m_ioctx.post( [this, data] () { if (m_in_connected) { bool const sending = !m_in_snd_buf.empty(); m_in_snd_buf.append(&data, 1U); if (!sending) start_send_in(); } }); } void send_out(u8 data) { m_ioctx.post( [this, data] () { if (m_out_sock.is_open()) { bool const sending = !m_out_snd_buf.empty(); m_out_snd_buf.append(&data, 1U); if (!sending) start_send_out(); } }); } private: class buffer { public: std::size_t append(u8 const *src, std::size_t len) { std::size_t used = 0U; if (!m_full) { if (m_wp >= m_rp) { used = std::min(m_buf.size() - m_wp, len); std::copy_n(&src[0], used, &m_buf[m_wp]); m_wp = (m_wp + used) % m_buf.size(); } std::size_t const block = std::min(len - used, m_rp - m_wp); if (block) { std::copy_n(&src[used], block, &m_buf[m_wp]); used += block; m_wp += block; } m_full = m_wp == m_rp; } return used; } std::size_t consume(std::size_t len) { std::size_t const delta = std::min( m_full ? m_buf.size() : ((m_wp + m_buf.size() - m_rp) % m_buf.size()), len); m_rp = (m_rp + delta) % m_buf.size(); if (delta) m_full = false; return delta; } auto content() const { return asio::buffer( &m_buf[m_rp], ((m_full || (m_wp < m_rp)) ? m_buf.size() : m_wp) - m_rp); } void clear() { m_wp = m_rp = 0U; } bool empty() const { return !m_full && (m_wp == m_rp); } private: unsigned m_wp = 0U, m_rp = 0U; bool m_full = false; std::array m_buf; }; template void logerror(Format &&fmt, Params &&... args) const { util::stream_format( std::cerr, "[%s] %s", m_device.tag(), util::string_format(std::forward(fmt), std::forward(args)...)); } bool forwarding() const { return BIT(m_route, 0, 2) == 1U; } void usart_queue(u8 const *src, std::size_t len) { unsigned wp = m_usart_wp.load(std::memory_order_relaxed); unsigned const rp = m_usart_rp.load(std::memory_order_acquire); if (wp >= rp) { std::size_t const block = std::min(m_usart_buf.size() - wp - (rp ? 0 : 1), len); std::copy_n(src, block, &m_usart_buf[wp]); src += block; len -= block; wp = (wp + block) % m_usart_buf.size(); } if (wp < rp) { std::size_t const block = std::min(rp - wp - 1, len); std::copy_n(src, block, &m_usart_buf[wp]); len -= block; wp += block; } m_usart_wp.store(wp, std::memory_order_release); if (len) LOG("USART buffer full, dropping %u bytes\n", len); } void start_accept() { LOG("Accepting OUT port connection on %s\n", *m_local); m_acceptor.async_accept( [this] (std::error_code const &err, asio::ip::tcp::socket sock) { if (err) { LOG("Error accepting OUT port connection: %s\n", err.message()); if (!m_stopping) start_accept(); } else { LOG("Accepted OUT port connection from %s\n", sock.remote_endpoint()); m_out_sock = std::move(sock); start_receive_out(); } }); } void start_connect() { LOG("Initiating IN port connection to %s\n", *m_remote); m_connect_timeout.expires_after(std::chrono::seconds(10)); m_connect_timeout.async_wait( [this] (std::error_code const &err) { if (!err && !m_stopping && !m_in_connected && m_in_sock.is_open()) { LOG("IN port connection timed out\n"); schedule_reconnect(); } }); m_in_sock.async_connect( *m_remote, [this] (std::error_code const &err) { m_connect_timeout.cancel(); if (err) { LOG("IN port connection error: %s\n", err.message()); if (!m_stopping) schedule_reconnect(); } else { LOG("IN port connection established\n"); m_in_connected = true; start_receive_in(); } }); } void schedule_reconnect() { LOG("Scheduling IN port reconnection\n"); std::error_code err; if (m_in_sock.is_open()) m_in_sock.close(err); m_connect_timeout.cancel(); m_connect_timeout.expires_after(std::chrono::seconds(10)); m_connect_timeout.async_wait( [this] (std::error_code const &err) { if (!err && !m_stopping) { std::error_code e; m_in_sock.open(m_remote->protocol(), e); if (e) { LOG("Error opening IN port socket: %s\n", e.message()); schedule_reconnect(); } else { start_connect(); } } }); } void start_receive_in() { m_in_sock.async_read_some( asio::buffer(m_in_rcv_buf), [this] (std::error_code const &err, std::size_t length) { if (err || !length) { if (err) LOG("Error receiving from IN port connection: %s\n", err.message()); else LOG("IN port connection lost\n"); m_in_connected = false; m_in_snd_buf.clear(); if (!m_stopping) schedule_reconnect(); } else { if (BIT(m_route, 0)) usart_queue(&m_in_rcv_buf[0], length); if (forwarding() && m_out_sock.is_open()) { bool const sending = !m_out_snd_buf.empty(); m_out_snd_buf.append(&m_in_rcv_buf[0], length); if (!sending) start_send_out(); } start_receive_in(); } }); } void start_receive_out() { m_out_sock.async_read_some( asio::buffer(m_out_rcv_buf), [this] (std::error_code const &err, std::size_t length) { if (err || !length) { if (err) LOG("Error receiving from OUT port connection: %s\n", err.message()); else LOG("OUT port connection lost\n"); m_out_snd_buf.clear(); if (!m_stopping) { std::error_code e; m_out_sock.close(e); start_accept(); } } else { if (BIT(m_route, 0, 2) == 2U) usart_queue(&m_out_rcv_buf[0], length); if (forwarding() && m_in_connected) { bool const sending = !m_in_snd_buf.empty(); m_in_snd_buf.append(&m_out_rcv_buf[0], length); if (!sending) start_send_in(); } start_receive_out(); } }); } void start_send_in() { m_in_sock.async_write_some( m_in_snd_buf.content(), [this] (std::error_code const &err, std::size_t length) { m_in_snd_buf.consume(length); if (err) { LOG("Error sending to IN port connection: %s\n", err.message().c_str()); m_in_connected = false; m_in_snd_buf.clear(); m_in_sock.close(); } else if (!m_in_snd_buf.empty()) { start_send_in(); } }); } void start_send_out() { m_out_sock.async_write_some( m_out_snd_buf.content(), [this] (std::error_code const &err, std::size_t length) { m_out_snd_buf.consume(length); if (err) { LOG("Error sending to OUT port connection: %s\n", err.message().c_str()); m_out_snd_buf.clear(); m_out_sock.close(); } else if (!m_out_snd_buf.empty()) { start_send_out(); } }); } cps2_comm_device &m_device; std::thread m_thread; asio::io_context m_ioctx; asio::ip::tcp::acceptor m_acceptor; asio::ip::tcp::socket m_in_sock; asio::ip::tcp::socket m_out_sock; asio::steady_timer m_connect_timeout; std::optional const m_local; std::optional const m_remote; u8 m_route; bool m_stopping; bool m_in_connected; std::atomic_uint m_usart_wp; std::atomic_uint m_usart_rp; buffer m_in_snd_buf; buffer m_out_snd_buf; std::array m_in_rcv_buf; std::array m_out_rcv_buf; std::array m_usart_buf; }; DEFINE_DEVICE_TYPE(CAPCOM_CPS2_COMM, cps2_comm_device, "cps2comm", "Capcom CPS-2 communication board") cps2_comm_device::cps2_comm_device(machine_config const &mconfig, char const *tag, device_t *owner, u32 clock) : device_t(mconfig, CAPCOM_CPS2_COMM, tag, owner, clock), m_config(*this, "CFG") { } u16 cps2_comm_device::usart_data_r(offs_t offset, u16 mem_mask) { if (!BIT(m_usart_status, 1) && m_context && !machine().side_effects_disabled() && m_context->usart_get(m_usart_rx_data)) m_usart_status |= 0x02U; LOG("%s: Read USART data %04X & 0x%04X\n", machine().describe_context(), m_usart_rx_data, mem_mask); if (ACCESSING_BITS_0_7 && !machine().side_effects_disabled()) m_usart_status &= ~0x02U; return m_usart_rx_data; } u16 cps2_comm_device::usart_status_r(offs_t offset, u16 mem_mask) { // 0: TxRDY transmit ready // 1: RxRDY receive ready // 2: TxEMP transmit buffers empty (TxD idle) // 3: PE receive parity error // 4: OVE receive buffer overrun error // 5: FE framing error // 6: SYNC/BRK synchronisation/break detect // 7: DSR inverted input if (!BIT(m_usart_status, 1) && m_context && !machine().side_effects_disabled() && m_context->usart_get(m_usart_rx_data)) m_usart_status |= 0x02U; LOG("%s: Read USART status %04X & 0x%04X\n", machine().describe_context(), m_usart_status, mem_mask); return m_usart_status; } void cps2_comm_device::usart_data_w(offs_t offset, u16 data, u16 mem_mask) { LOG("%s: Write USART data 0x%04X & 0x%04X\n", machine().describe_context(), data, mem_mask); if (ACCESSING_BITS_0_7 && m_context) { switch (BIT(m_route, 0, 2)) { case 0: break; case 1: case 3: if (m_is_head) { m_usart_rx_data = data & 0x00ffU; if (BIT(m_usart_status, 1)) m_usart_status |= 0x10U; // set OVE else m_usart_status |= 0x02U; // set RxRDY } else { m_context->send_in(data & 0x00ffU); } break; case 2: m_context->send_out(data & 0x00ffU); break; } } } void cps2_comm_device::usart_control_w(offs_t offset, u16 data, u16 mem_mask) { if (ACCESSING_BITS_0_7) { switch (m_usart_control_phase) { case CTRL_MODE: m_usart_mode = data & 0x00ffU; if (!BIT(m_usart_mode, 0, 2)) { LOG("%s: USART mode = 0x%02X: Synchronous, %d-bit, %s Parity, %s Sync Detect, %d Sync Characters\n", machine().describe_context(), m_usart_mode, std::array{ 5, 6, 7, 8 }[BIT(m_usart_mode, 2, 2)], std::array{ "No", "Odd", "No", "Even" }[BIT(m_usart_mode, 4, 2)], BIT(m_usart_mode, 6) ? "External" : "Internal", BIT(m_usart_mode, 7) ? 1 : 2); m_usart_control_phase = CTRL_SYNC1; } else { LOG("%s: USART mode = 0x%02X: Asynchronous, x %d Clock, %d-bit, %s Parity, %s Transmit Stop Bits\n", machine().describe_context(), m_usart_mode, std::array{ -1, 1, 16, 64 }[BIT(m_usart_mode, 0, 2)], std::array{ 5, 6, 7, 8 }[BIT(m_usart_mode, 2, 2)], std::array{ "No", "Odd", "No", "Even" }[BIT(m_usart_mode, 4, 2)], std::array{ "Illegal", "1", "1-1/2", "2" }[BIT(m_usart_mode, 6, 2)]); m_usart_control_phase = CTRL_COMMAND; m_usart_status |= 0x01U; } break; case CTRL_SYNC1: m_usart_sync_char[0] = data & 0x00ffU; LOG("%s: USART sync character 1 = 0x%02X\n", machine().describe_context(), m_usart_sync_char[0]); if (BIT(m_usart_mode, 7)) { m_usart_control_phase = CTRL_COMMAND; m_usart_status |= 0x01U; } else { m_usart_control_phase = CTRL_SYNC2; } break; case CTRL_SYNC2: m_usart_sync_char[1] = data & 0x00ffU; LOG("%s: USART sync character 1 = 0x%02X\n", machine().describe_context(), m_usart_sync_char[1]); m_usart_control_phase = CTRL_COMMAND; m_usart_status |= 0x01U; break; case CTRL_COMMAND: LOG("%s: USART command 0x%02X%s /DTR=%u%s%s%s /RTS=%u%s%s\n", machine().describe_context(), data & 0x00ff, BIT(data, 0) ? " TxEN" : "", BIT(~data, 1), BIT(data, 2) ? " RxEN" : "", BIT(data, 3) ? " SBRK" : "", BIT(data, 4) ? " ECL" : "", BIT(~data, 5), BIT(data, 6) ? " SRES" : "", BIT(data, 7) ? " EH" : ""); if (BIT(data, 4)) m_usart_status &= ~0x38U; if (BIT(data, 6)) { m_usart_control_phase = CTRL_MODE; m_usart_status = 0x00U; } break; } } } void cps2_comm_device::route_w(offs_t offset, u16 data, u16 mem_mask) { if (ACCESSING_BITS_0_7) { m_route = data & 0x00ffU; if (m_context) m_context->set_routing(m_route); LOG("%s: Write routing control 0x%04X: %s data rate, %s -> IN, %s -> OUT, %s -> RxD\n", machine().describe_context(), m_route, BIT(m_route, 4) ? "low" : "high", std::array{ "1", "TxD & OUT", "1", "TxD" }[BIT(m_route, 0, 2)], std::array{ "1", "IN", "TxD", "1" }[BIT(m_route, 0, 2)], std::array{ "1", "IN", "OUT", "IN" }[BIT(m_route, 0, 2)]); } } ioport_constructor cps2_comm_device::device_input_ports() const { return INPUT_PORTS_NAME(cps2comm); } void cps2_comm_device::device_start() { m_usart_mode = 0x00U; std::fill(std::begin(m_usart_sync_char), std::end(m_usart_sync_char), 0U); m_usart_rx_data = 0U; m_is_head = false; save_item(NAME(m_usart_control_phase)); save_item(NAME(m_usart_mode)); save_item(NAME(m_usart_sync_char)); save_item(NAME(m_usart_status)); save_item(NAME(m_usart_rx_data)); save_item(NAME(m_route)); } void cps2_comm_device::device_reset() { if (!m_context) start_comm(); m_usart_control_phase = CTRL_MODE; m_usart_status = 0x00U; m_route = 0x00U; if (m_context) m_context->set_routing(m_route); } void cps2_comm_device::device_stop() { if (m_context) { m_context->stop(); m_context.reset(); } } void cps2_comm_device::start_comm() { if (!BIT(m_config->read(), 0)) return; auto const &opts = mconfig().options(); std::error_code err; std::istringstream parsestr; parsestr.imbue(std::locale::classic()); std::optional local; if (*opts.comm_localhost()) { asio::ip::address const bindaddr = asio::ip::make_address(opts.comm_localhost(), err); if (err) { osd_printf_error("[%s] invalid local IP address %s, disabling communication.\n", tag(), opts.comm_localhost()); return; } parsestr.str(opts.comm_localport()); parsestr.seekg(0, std::ios_base::beg); asio::ip::port_type bindport; parsestr >> bindport; if (!parsestr || !bindport) { osd_printf_error("[%s] invalid local TCP port %s, disabling communication.\n", tag(), opts.comm_localport()); return; } local.emplace(bindaddr, bindport); } std::optional remote; if (*opts.comm_remotehost()) { asio::ip::address const connaddr = asio::ip::make_address(opts.comm_remotehost(), err); if (err) { osd_printf_error("[%s] invalid remote address %s, disabling communication.\n", tag(), opts.comm_remotehost()); return; } parsestr.str(opts.comm_remoteport()); parsestr.seekg(0, std::ios_base::beg); asio::ip::port_type connport; parsestr >> connport; if (!parsestr || !connport) { osd_printf_error("[%s] invalid remote TCP port %s, disabling communication.\n", tag(), opts.comm_remoteport()); return; } remote.emplace(connaddr, connport); } else { m_is_head = true; } if (!local && !remote) { osd_printf_error("[%s] no TCP addresses configured, disabling communication.\n", tag()); return; } auto ctx = std::make_unique(*this, local, remote); err = ctx->start(); if (err) { osd_printf_error("[%s] error opening/binding sockets, disabling communication (%s).\n", tag(), err.message()); return; } m_context = std::move(ctx); }