// license:LGPL-2.1+ // copyright-holders:Michael Zapf /**************************************************************************** TIPI adapter card designed and realized by Matthew Splett (aka jedimatt42), Peter and others The TIPI system (pronouced "tipee") is an expansion for the TI-99/4A and other computers of this family. It consists of a Raspberry Pi computer attached to an adapter card that fits into the PEB (Peripheral Expansion Box) or to the console side connector. The adapter card provides an interface between the TI system and the Raspberry Pi, in particular for exchanging files. More than that, features of the Raspberry like network connectivity, may be offered to the TI system. The general concept is that there is a set of memory-mapped ports between the TI and the Raspberry Pi. 0x5ff9 (RCIN): PI Control Signal (input from RPi) 0x5ffb (RDIN): PI Data (input) 0x5ffd (TCOUT): TI Control Signal (output to RPi) 0x5fff (TDOUT): TI Data (output) CRU map ------- 1x00: Activate the card and its ROM (x=0...f) 1x02: Send RESET to Raspberry Pi Raspberry connection -------------------- The good thing in terms of emulation is that the Raspberry Pi is completely outside of the emulation. It allows for connecting via a WebSocket, which is especially provided for emulators. In the real machine, the Raspberry Pi gets the data from the adapter card. It is recommended to run a virtual Raspberry Pi as a QEMU image. An image is available on the TIPI website. The connection is specified by -conn rpi.[:], like "-conn rpi.localhost" or "-conn rpi.some.host.net" This will create a file in the current directory of that name, which is a side effect of the MAME image handling. The port default is 9901. Mouse support ------------- The websocket server accepts messages for mouse movements and button actions from the mouse input of the connected emulator. However, in MAME, mouse input is only available if there is a proper mouse device, like the busmouse or the Mechatronics mouse. Hence, we cannot send any mouse input to the Raspberry without one of these mice connected. However, if a mouse is connected to the real or virtual Raspberry Pi, the mouse operations can be polled from the Raspberry in the intended way. Logging ------- Since we use a background thread for the websocket client, and logging is not thread-safe, log output may look broken at some times. Michael Zapf March 20222 References [1] TIPI description on Github: https://github.com/jedimatt42/tipi *****************************************************************************/ #include "emu.h" #include "tipi.h" #define LOG_WARN (1U<<1) #define LOG_EPROM (1U<<2) #define LOG_CRU (1U<<3) #define LOG_PORTS (1U<<4) #define LOG_RPI (1U<<5) #define LOG_QUEUE (1U<<6) #define LOG_PROT (1U<<7) #define LOG_DETAIL (1U<<8) #define VERBOSE ( LOG_GENERAL | LOG_WARN ) #define RASPI "rpi" #include "logmacro.h" DEFINE_DEVICE_TYPE(TI99_TIPI, bus::ti99::peb::tipi_card_device, "ti99_tipi", "TIPI card") DEFINE_DEVICE_TYPE(TI99_TIPI_RPI, bus::ti99::peb::tipi_attached_device, "ti99_tipi_atttached", "Connection to external device") namespace bus::ti99::peb { tipi_card_device::tipi_card_device(const machine_config &mconfig, const char *tag, device_t *owner, const XTAL &clock) : device_t(mconfig, TI99_TIPI, tag, owner, clock), device_ti99_peribox_card_interface(mconfig, *this), m_rpi(*this, RASPI), m_address(0), m_dsr(false), m_portaccess(false), m_waitinit(false), m_syncmode(true), m_rpiconn(false), m_tc(0), m_rd(0), m_lasttc(0) { } void tipi_card_device::setaddress_dbin(offs_t offset, int state) { if (machine().side_effects_disabled()) return; m_address = offset; m_dsr = false; m_portaccess = false; if (in_dsr_space(offset, true) && m_selected) { m_portaccess = ((offset & 0x1ff8)==0x1ff8); m_dsr = !m_portaccess; } } void tipi_card_device::readz(offs_t offset, uint8_t *value) { if (machine().side_effects_disabled()) { debug_read(offset, value); return; } if (m_dsr) { // Lines A0-A12 directly connected to the EPROM (chip pin order) // Lines A13-A15 connected to PLD (currently not used) int base = 0; uint8_t* rom = &m_eprom[base | (m_address & 0x1fff)]; *value = *rom; if (WORD_ALIGNED(m_address)) { // Do logging by 16 bit words, as we typically have instructions in // the eprom uint16_t val = (*rom << 8) | (*(rom+1)); LOGMASKED(LOG_EPROM, "DSR: %04x -> %04x\n", m_address & 0xffff, val); } } if (m_portaccess) { if ((m_address & 5)==1) { int val = 0; if (m_address & 2) { LOGMASKED(LOG_PORTS, "RDIN -> %02x\n", m_rd); val = m_rd; } else { val = m_rc; LOGMASKED(LOG_PORTS, "RCIN -> %02x\n", val); } *value = val; } } } void tipi_card_device::write(offs_t offset, uint8_t data) { if (machine().side_effects_disabled()) { debug_write(offset, data); return; } if (m_dsr) { LOGMASKED(LOG_WARN, "Writing to DSR space: %04x <- %02x\n", m_address & 0xffff, data); } if (m_portaccess) { if (m_address & 1) { if (m_address & 2) { LOGMASKED(LOG_PORTS, "TDOUT <- %02x\n", data); set_td(data); } else { LOGMASKED(LOG_PORTS, "TCOUT <- %02x\n", data); set_tc(data); } } } } void tipi_card_device::set_td(u8 data) { m_td = data; } void tipi_card_device::set_tc(u8 data) { if (m_tc != data) { m_tc = data; m_lasttc = 0; process_message(); } } /* Debugger access. */ void tipi_card_device::debug_read(offs_t offset, uint8_t* value) { int base = 0; if (in_dsr_space(offset, true) && m_selected) { if ((offset & 0x1ff8)!=0x1ff8) // not in mapping area { uint8_t* rom = &m_eprom[base | (offset & 0x1fff)]; *value = *rom; } } } /* Debugger access. */ void tipi_card_device::debug_write(offs_t offset, uint8_t data) { // No write allowed } /* CRU read access */ void tipi_card_device::crureadz(offs_t offset, uint8_t *value) { // No CRU read } /* CRU write access. */ void tipi_card_device::cruwrite(offs_t offset, uint8_t data) { if ((offset & 0xff00) == m_cru_base) { int bit = (offset >> 1) & 0x1f; switch (bit) { case 0: // card activation LOGMASKED(LOG_CRU, "DSR %s\n", (data!=0)? "on" : "off"); m_selected = (data != 0); break; case 1: LOGMASKED(LOG_CRU, "CRU Bit 1 %s\n", (data!=0)? "on" : "off"); if (data != 0) { LOGMASKED(LOG_RPI, "Sending RESET\n"); send("RESET"); } break; default: LOGMASKED(LOG_CRU, "Unknown CRU Bit %d\n", bit); break; } } } void tipi_card_device::send(const char* message) { if (m_rpiconn && m_connected) { *m_send_stream << message; m_wsclient->send(m_send_stream); } } void tipi_card_device::send(u8* message, int len) { if (m_rpiconn && m_connected) { for (int i=0; i < len; i++) { LOGMASKED(LOG_DETAIL, "Sending byte %02x\n", message[i]); *m_send_stream << message[i]; } m_wsclient->send(m_send_stream, nullptr, 130); // binary (10000010) } } enum { TSWB = 0x02, TSRB = 0x06, TSRSET = 0xf1 }; /* Messages from server: "RD=xxx" "RC=xxx" processMessage occurs for each incoming message and each time that TC has been changed Write a message to RPi: for i=-2..(n-1) Write byte to TD Write (02 | (i & 1)) to TC Byte sequence: len1 len2 m0 m1 m2 m3 ... m(n-1), where n = len1<<8 | len2; Message will be written to RPi when complete Read a message from RPi: n = message.length n = n1<<8 | n2 Write 06 to TC n1 = Read byte from TD Write 07 to TC n2 = Read byte from TD for i=0..(n-1) Write (06 | (i & 1)) to TC Read byte from TD */ void tipi_card_device::process_message() { if (m_tc == TSRSET) // Reset command { LOGMASKED(LOG_PROT, "Reset\n"); m_rpimessage = nullptr; m_msgindex = -2; m_rc = m_tc; m_lasttc = 0; if (m_syncmode) { LOGMASKED(LOG_PROT, "Sending SYNC\n"); send("SYNC"); // only for SYNC mode } } else { if (m_lasttc != m_tc) { if ((m_tc & 0xfe) == TSWB) // 02 and 03: write byte (LSB as strobe) { switch (m_msgindex) { case -2: m_msglength = m_td << 8; break; case -1: m_msglength |= m_td; m_rpimessage = std::make_unique(m_msglength); break; default: if (m_rpimessage != nullptr) m_rpimessage[m_msgindex] = m_td; } m_msgindex++; if (m_msgindex == m_msglength) { // Message is complete - transmit it LOGMASKED(LOG_RPI, "Sending message, length %d\n", m_msglength); send(m_rpimessage.get(), m_msglength); m_rpimessage = nullptr; } else m_lasttc = m_tc; m_rc = m_tc; // Auto-acknowledge } else { if ((m_tc & 0xfe) == TSRB) // 06 and 07: read byte (LSB as strobe) { if (!m_indqueue.empty()) { m_rc = m_tc; // Auto-acknowledge m_lasttc = m_tc; m_rd = m_indqueue.front(); m_indqueue.pop(); } } } } } } /* Syntax for connecting is -conn rpi.[:] Default for port is 9901. */ TIMER_CALLBACK_MEMBER(tipi_card_device::open_websocket) { if (m_rpiconn) { // We are just interested in the file name // Cannot call it "socket.xxx" because this will make sdlsocket open // the connection std::string sconn(m_rpi->filename()); int pos = sconn.find("rpi."); if (pos != -1) { std::string shost(sconn.substr(pos + 4)); if (shost.find(":")==-1) shost += ":9901"; shost += "/tipi"; LOG("Trying to connect to Raspberry Pi at %s\n", shost.c_str()); m_wsclient = std::make_unique(shost); webpp::ws_client* wsc = m_wsclient.get(); wsc->on_open = [this]() { websocket_opened(); }; wsc->on_message = [this](std::shared_ptr message) { websocket_incoming(message); }; wsc->on_close = [this](int i, const std::string& msg) { websocket_closed(i, msg); }; wsc->on_error = [this](const std::error_code& code) { websocket_error(code); }; m_send_stream = std::make_shared(); asio::thread listener([this]() { m_wsclient->start(); }); } else fatalerror("Invalid TIPI connection address. Syntax: -conn rpi.[:]\n"); } } void tipi_card_device::websocket_opened() { LOG("Connection established\n"); m_connected = true; m_rc = 0; if (m_waitinit) m_slot->set_ready(ASSERT_LINE); } void tipi_card_device::websocket_incoming(std::shared_ptr message) { // Caution: Message is an istream, so the string method consumes the Message // Size is not changed, nor is fin_rsv_opcode std::string msg; switch (message->fin_rsv_opcode & 0x0f) { case 1: msg = message->string(); // Text message is "RD=xxx" or "RC=xxx" with xxx=decimal number LOGMASKED(LOG_RPI, "Got text message: %s\n", msg); if (msg.find("RD=")==0) { m_indqueue.push(std::stoi(message->string().substr(3))); } if (msg.find("RC=")==0) { m_rc = std::stoi(message->string().substr(3)); } if (msg.find("ASYNC")==0) { LOG("TIPI server offers ASYNC communication\n"); m_syncmode = false; } break; case 2: LOGMASKED(LOG_RPI, "Got binary message, length %d\n", message->size()); // Enqueue the incoming message // as n1 n2 msg, where n1 and n2 are the length of the message, // and msg is the byte sequence of the message. { u8 len1 = (message->size()>>8) & 0xff; u8 len2 = message->size() & 0xff; m_indqueue.push(len1); LOGMASKED(LOG_QUEUE, "<- %02x (len1) [%d]\n", len1, m_indqueue.size()); m_indqueue.push(len2); LOGMASKED(LOG_QUEUE, "<- %02x (len2) [%d]\n", len2, m_indqueue.size()); } for (int i=0; i < message->size(); i++) { u8 msgbyte = (u8)message->get(); m_indqueue.push(msgbyte); LOGMASKED(LOG_QUEUE, "<- %02x [%d]\n", msgbyte, m_indqueue.size()); } process_message(); break; default: LOGMASKED(LOG_RPI, "Got unknown message\n"); break; } } void tipi_card_device::websocket_error(const std::error_code& code) { if (code.value() == std::make_error_code(std::errc::connection_reset).value()) { m_wsclient->stop(); if (m_attempts > 0) { m_attempts--; LOGMASKED(LOG_WARN, "Error, connection reset. Retrying.\n"); m_restart_timer->adjust(attotime::from_msec(2500)); } else { LOGMASKED(LOG_WARN, "Connection has been reset several times; giving up\n"); m_connected = false; } } else { // End-of-file (2) occurs when the Pi is rebooted LOGMASKED(LOG_WARN, "Got error: %s (%d)\n", code.message().c_str(), code.value()); // Reconnection is not implemented on the Pi side for emulations, // so this may cause the config program to wait forever // Needs to be documented m_restart_timer->adjust(attotime::from_msec(20000)); m_attempts = 20; } } void tipi_card_device::websocket_closed(int i, const std::string& msg) { LOG("Closing connection\n"); if (m_waitinit) m_slot->set_ready(CLEAR_LINE); // Open it again after 3 secs m_wsclient->stop(); m_restart_timer->adjust(attotime::from_msec(3000)); m_attempts = 5; } void tipi_card_device::device_start() { m_eprom = memregion(TI99_DSRROM)->base(); save_item(NAME(m_address)); save_item(NAME(m_dsr)); save_item(NAME(m_portaccess)); save_item(NAME(m_msgindex)); save_item(NAME(m_msglength)); save_item(NAME(m_attempts)); save_item(NAME(m_connected)); save_item(NAME(m_tc)); save_item(NAME(m_td)); save_item(NAME(m_rc)); save_item(NAME(m_lasttc)); m_restart_timer = timer_alloc(FUNC(tipi_card_device::open_websocket), this); m_rpiconn = (m_rpi->exists()); } void tipi_card_device::device_reset() { m_cru_base = (ioport("SW1")->read()) << 8; m_waitinit = (ioport("WAIT")->read()!=0); m_syncmode = true; m_address = 0; m_dsr = false; m_portaccess = false; if (!m_rpiconn) LOGMASKED(LOG_WARN, "No Raspberry Pi connected\n"); // READY=0 is ignored at this point because the CPU is being reset at this time open_websocket(0); m_connected = false; m_lasttc = 0; m_attempts = 5; } void tipi_card_device::device_stop() { // MZ: Without this I'm getting segfaults/list corruption // when leaving the emulation m_wsclient->stop(); LOG("Stopping TIPI\n"); } /* The CRU address base for the card. For the Geneve OS version 7.30, CRU base 1800 is mandatory. */ INPUT_PORTS_START( tipi ) PORT_START("SW1") PORT_DIPNAME(0x1f, 0x18, "CRU base") PORT_DIPSETTING(0x10, "1000") PORT_DIPSETTING(0x11, "1100") PORT_DIPSETTING(0x12, "1200") PORT_DIPSETTING(0x13, "1300") PORT_DIPSETTING(0x14, "1400") PORT_DIPSETTING(0x15, "1500") PORT_DIPSETTING(0x16, "1600") PORT_DIPSETTING(0x17, "1700") PORT_DIPSETTING(0x18, "1800") // Default setting PORT_DIPSETTING(0x19, "1900") PORT_DIPSETTING(0x1a, "1a00") PORT_DIPSETTING(0x1b, "1b00") PORT_DIPSETTING(0x1c, "1c00") PORT_DIPSETTING(0x1d, "1d00") PORT_DIPSETTING(0x1e, "1e00") PORT_DIPSETTING(0x1f, "1f00") PORT_START("WAIT") PORT_CONFNAME(0x01, 0x00, "Wait for initialization") PORT_CONFSETTING( 0x00, DEF_STR( Off ) ) PORT_CONFSETTING( 0x01, DEF_STR( On ) ) INPUT_PORTS_END void tipi_card_device::device_add_mconfig(machine_config &config) { TI99_TIPI_RPI(config, m_rpi); } ROM_START( tipi ) ROM_REGION(0x8000, TI99_DSRROM, 0) ROM_LOAD("tipidsr.u2", 0x0000, 0x8000, CRC(a54e65af) SHA1(f930e4b079a5b6b24dc20f262e9c599d2051b6be)) ROM_END const tiny_rom_entry *tipi_card_device::device_rom_region() const { return ROM_NAME( tipi ); } ioport_constructor tipi_card_device::device_input_ports() const { return INPUT_PORTS_NAME( tipi ); } /****************************************************************************/ /* Connection to the external device, a Raspberry PI */ tipi_attached_device::tipi_attached_device(const machine_config &mconfig, const char *tag, device_t *owner, const XTAL &clock) : device_t(mconfig, TI99_TIPI_RPI, tag, owner, clock), device_image_interface(mconfig, *this) { } /* Initialize connection */ image_init_result tipi_attached_device::call_load() { return image_init_result::PASS; // OK } void tipi_attached_device::call_unload() { } } // end namespace bus::ti99::peb