/* * Copyright (C) 2002-2021 The DOSBox Team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "dosbox.h" #if C_DIRECTSERIAL #include "logging.h" #include "serialport.h" #include "directserial.h" #include "misc_util.h" #include "pic.h" #include "libserial.h" #include /* This is a serial passthrough class. Its amazingly simple to */ /* write now that the serial ports themselves were abstracted out */ static double tap_now_us() { using namespace std::chrono; return duration( steady_clock::now().time_since_epoch()).count(); } /* One log line per byte on the wire. Host-relative microseconds tell the * real-time story (the RIO's ACK deadline is wall clock); PIC_FullIndex() * (emulated ms) ties each byte to game-side timing. */ void CDirectSerial::tapLine(char dir, unsigned val, unsigned err) { if (!tap_fp) return; double us = tap_now_us() - tap_t0_us; if (err) fprintf(tap_fp, "%12.1f %11.3f %c %02X err=%02X\n", us, PIC_FullIndex(), dir, val, err); else fprintf(tap_fp, "%12.1f %11.3f %c %02X\n", us, PIC_FullIndex(), dir, val); fflush(tap_fp); } CDirectSerial::CDirectSerial (Bitu id, CommandLine* cmd) :CSerial (id, cmd) { InstallationSuccessful = false; comport = nullptr; rx_retry = 0; rx_retry_max = 0; rx_state = 0; std::string tmpstring; if(!cmd->FindStringBegin("realport:",tmpstring,false)) return; LOG_MSG ("Serial%d: Opening %s", (int)(COMNUMBER), tmpstring.c_str()); if(!SERIAL_open(tmpstring.c_str(), &comport)) { char errorbuffer[256]; SERIAL_getErrorString(errorbuffer, sizeof(errorbuffer)); LOG_MSG("Serial%d: Serial Port \"%s\" could not be opened.", (int)(COMNUMBER), tmpstring.c_str()); LOG_MSG("%s",errorbuffer); return; } // rxdelay: How many milliseconds to wait before causing an // overflow when the application is unresponsive. if(getBituSubstring("rxdelay:", &rx_retry_max, cmd)) { if(!(rx_retry_max<=10000)) { rx_retry_max=0; } } // rxpollus: host-port receive polling tick in MICROSECONDS (50..1000, // default 1000 = the stock 1ms tick). Lower values discover incoming // bytes sooner -- needed for hard-real-time serial peripherals like the // VWE RIO cockpit board, which drops comms on ACKs late by a few ms. Bitu rx_poll_us = 0; if(getBituSubstring("rxpollus:", &rx_poll_us, cmd)) { if(rx_poll_us < 50) rx_poll_us = 50; if(rx_poll_us > 1000) rx_poll_us = 1000; rx_poll_ms = (float)rx_poll_us / 1000.0f; LOG_MSG("Serial%d: receive poll tick %uus", (int)(COMNUMBER), (unsigned)rx_poll_us); } // rxburst: deliver host-buffered received bytes times faster than // emulated wire speed (they already paid wire time on the real cable). Bitu rx_burst = 0; if(getBituSubstring("rxburst:", &rx_burst, cmd)) { if(rx_burst < 1) rx_burst = 1; if(rx_burst > 64) rx_burst = 64; rx_burst_div = (float)rx_burst; LOG_MSG("Serial%d: receive burst delivery x%u", (int)(COMNUMBER), (unsigned)rx_burst); } // RIO_TAP= (host env, like the VPX_* vars): append one line per // TX/RX byte. Appends across runs; each open writes a header line. const char *tp = getenv("RIO_TAP"); if (tp && tp[0]) { tap_fp = fopen(tp, "a"); if (tap_fp) { tap_t0_us = tap_now_us(); fprintf(tap_fp, "# COM%d tap open (host-us rel | emu-ms | dir | byte)\n", (int)COMNUMBER); fflush(tap_fp); LOG_MSG("Serial%d: RIO_TAP logging to '%s'", (int)COMNUMBER, tp); } else { LOG_MSG("Serial%d: RIO_TAP cannot open '%s'", (int)COMNUMBER, tp); } } CSerial::Init_Registers(); InstallationSuccessful = true; rx_state = D_RX_IDLE; setEvent(SERIAL_POLLING_EVENT, rx_poll_ms); // receive tick } CDirectSerial::~CDirectSerial () { if(comport) SERIAL_close(comport); if(tap_fp) fclose(tap_fp); // We do not use own events so we don't have to clear them. } // CanReceive: true:UART part has room left // doReceive: true:there was really a byte to receive // rx_retry is incremented in polling events // in POLLING_EVENT: always add new polling event // D_RX_IDLE + CanReceive + doReceive -> D_RX_WAIT , add RX_EVENT // D_RX_IDLE + CanReceive + not doReceive -> D_RX_IDLE // D_RX_IDLE + not CanReceive -> D_RX_BLOCKED, add RX_EVENT // D_RX_BLOCKED + CanReceive + doReceive -> D_RX_FASTWAIT, rem RX_EVENT // rx_retry=0 , add RX_EVENT // D_RX_BLOCKED + CanReceive + !doReceive -> D_RX_IDLE, rem RX_EVENT // rx_retry=0 // D_RX_BLOCKED + !CanReceive + doReceive + retry < max -> D_RX_BLOCKED, rx_retry++ // D_RX_BLOCKED + !CanReceive + doReceive + retry >=max -> rx_retry=0 // to be continued... void CDirectSerial::handleUpperEvent(uint16_t type) { /* #if SERIAL_DEBUG const char* s; const char* s2; switch(type) { case SERIAL_POLLING_EVENT: s = "POLLING_EVENT"; break; case SERIAL_RX_EVENT: s = "RX_EVENT"; break; case SERIAL_TX_EVENT: s = "TX_EVENT"; break; case SERIAL_THR_EVENT: s = "THR_EVENT"; break; } switch(rx_state) { case D_RX_IDLE: s2 = "RX_IDLE"; break; case D_RX_WAIT: s2 = "RX_WAIT"; break; case D_RX_BLOCKED: s2 = "RX_BLOCKED"; break; case D_RX_FASTWAIT: s2 = "RX_FASTWAIT"; break; } log_ser(dbg_aux,"Directserial: Event enter %s, %s",s,s2); #endif */ switch(type) { case SERIAL_POLLING_EVENT: { setEvent(SERIAL_POLLING_EVENT, rx_poll_ms); // update Modem input line states switch(rx_state) { case D_RX_IDLE: if(CanReceiveByte()) { if(doReceive()) { // a byte was received rx_state=D_RX_WAIT; setEvent(SERIAL_RX_EVENT, bytetime*0.9f/rx_burst_div); } // else still idle } else { #if SERIAL_DEBUG if(!dbgmsg_poll_block) { log_ser(dbg_aux,"Directserial: block on polling."); dbgmsg_poll_block=true; } #endif rx_state=D_RX_BLOCKED; // have both delays (1ms + bytetime) setEvent(SERIAL_RX_EVENT, bytetime*0.9f/rx_burst_div); } break; case D_RX_BLOCKED: // one timeout tick if(!CanReceiveByte()) { rx_retry++; if(rx_retry>=rx_retry_max) { // it has timed out: rx_retry=0; removeEvent(SERIAL_RX_EVENT); if(doReceive()) { // read away everything // this will set overrun errors while(doReceive()); rx_state=D_RX_WAIT; setEvent(SERIAL_RX_EVENT, bytetime*0.9f/rx_burst_div); } else { // much trouble about nothing rx_state=D_RX_IDLE; } } // else wait further } else { // good: we can receive again #if SERIAL_DEBUG dbgmsg_poll_block=false; dbgmsg_rx_block=false; #endif removeEvent(SERIAL_RX_EVENT); rx_retry=0; if(doReceive()) { rx_state=D_RX_FASTWAIT; setEvent(SERIAL_RX_EVENT, bytetime*0.65f/rx_burst_div); } else { // much trouble about nothing rx_state=D_RX_IDLE; } } break; case D_RX_WAIT: case D_RX_FASTWAIT: break; } updateMSR(); break; } case SERIAL_RX_EVENT: { switch(rx_state) { case D_RX_IDLE: LOG_MSG("internal error in directserial"); break; case D_RX_BLOCKED: // try to receive case D_RX_WAIT: case D_RX_FASTWAIT: if(CanReceiveByte()) { // just works or unblocked rx_retry=0; // not waiting anymore if(doReceive()) { if(rx_state==D_RX_WAIT) setEvent(SERIAL_RX_EVENT, bytetime*0.9f/rx_burst_div); else { // maybe unblocked rx_state=D_RX_FASTWAIT; setEvent(SERIAL_RX_EVENT, bytetime*0.65f/rx_burst_div); } } else { // didn't receive anything rx_state=D_RX_IDLE; } } else { // blocking now or still blocked #if SERIAL_DEBUG if(rx_state==D_RX_BLOCKED) { if(!dbgmsg_rx_block) { log_ser(dbg_aux,"Directserial: rx still blocked (retry=%d)",rx_retry); dbgmsg_rx_block=true; } } else log_ser(dbg_aux,"Directserial: block on continued rx (retry=%d).",rx_retry); #endif setEvent(SERIAL_RX_EVENT, bytetime*0.65f/rx_burst_div); rx_state=D_RX_BLOCKED; } break; } updateMSR(); break; } case SERIAL_TX_EVENT: { // Maybe echo circuit works a bit better this way if(rx_state==D_RX_IDLE && CanReceiveByte()) { if(doReceive()) { // a byte was received rx_state=D_RX_WAIT; setEvent(SERIAL_RX_EVENT, bytetime*0.9f/rx_burst_div); } } ByteTransmitted(); updateMSR(); break; } case SERIAL_THR_EVENT: { ByteTransmitting(); setEvent(SERIAL_TX_EVENT,bytetime*1.1f); break; } } /* #if SERIAL_DEBUG switch(type) { case SERIAL_POLLING_EVENT: s = "POLLING_EVENT"; break; case SERIAL_RX_EVENT: s = "RX_EVENT"; break; case SERIAL_TX_EVENT: s = "TX_EVENT"; break; case SERIAL_THR_EVENT: s = "THR_EVENT"; break; } switch(rx_state) { case D_RX_IDLE: s2 = "RX_IDLE"; break; case D_RX_WAIT: s2 = "RX_WAIT"; break; case D_RX_BLOCKED: s2 = "RX_BLOCKED"; break; case D_RX_FASTWAIT: s2 = "RX_FASTWAIT"; break; } log_ser(dbg_aux,"Directserial: Event exit %s, %s",s,s2); #endif*/ } bool CDirectSerial::doReceive() { int value = SERIAL_getextchar(comport); if(value) { tapLine('R', (unsigned)(value & 0xff), (unsigned)((value >> 8) & 0xff)); receiveByteEx((uint8_t)(value&0xff),(uint8_t)((value&0xff00)>>8)); return true; } return false; } // updatePortConfig is called when emulated app changes the serial port // parameters baudrate, stopbits, number of databits, parity. void CDirectSerial::updatePortConfig (uint16_t divider, uint8_t lcr) { uint8_t parity = 0; switch ((lcr & 0x38)>>3) { case 0x1: parity='o'; break; case 0x3: parity='e'; break; case 0x5: parity='m'; break; case 0x7: parity='s'; break; default: parity='n'; break; } uint8_t bytelength = (lcr & 0x3)+5; // baudrate Bitu baudrate; if(divider==0) baudrate=115200u; else baudrate = 115200u / divider; // stopbits uint8_t stopbits; if (lcr & 0x4) { if (bytelength == 5) stopbits = SERIAL_15STOP; else stopbits = SERIAL_2STOP; } else stopbits = SERIAL_1STOP; if (tap_fp) { fprintf(tap_fp, "# %12.1f %11.3f config %u baud %d%c%d\n", tap_now_us() - tap_t0_us, PIC_FullIndex(), (unsigned)baudrate, (int)bytelength, (char)parity, (int)stopbits); fflush(tap_fp); } if(!SERIAL_setCommParameters(comport, (int)baudrate, (char)parity, (char)stopbits, (char)bytelength)) { #if SERIAL_DEBUG log_ser(dbg_aux,"Serial port settings not supported by host." ); #endif LOG_MSG ("Serial%d: Desired serial mode not supported (%d,%d,%c,%d)", (int)(COMNUMBER),(int)baudrate,(int)bytelength,parity,(int)stopbits); } CDirectSerial::setRTSDTR(getRTS(), getDTR()); } void CDirectSerial::updateMSR () { int new_status = SERIAL_getmodemstatus(comport); setCTS((new_status&SERIAL_CTS)? true:false); setDSR((new_status&SERIAL_DSR)? true:false); setRI((new_status&SERIAL_RI)? true:false); setCD((new_status&SERIAL_CD)? true:false); } void CDirectSerial::transmitByte (uint8_t val, bool first) { tapLine('T', val, 0); if(!SERIAL_sendchar(comport, (char)val)) LOG_MSG("Serial%d: COM port error: write failed!", (int)COMNUMBER); if(first) setEvent(SERIAL_THR_EVENT, bytetime/8); else setEvent(SERIAL_TX_EVENT, bytetime); } // setBreak(val) switches break on or off void CDirectSerial::setBreak (bool value) { if (tap_fp) { fprintf(tap_fp, "# %12.1f %11.3f break=%d\n", tap_now_us() - tap_t0_us, PIC_FullIndex(), value ? 1 : 0); fflush(tap_fp); } SERIAL_setBREAK(comport,value); } // updateModemControlLines(mcr) sets DTR and RTS. void CDirectSerial::setRTSDTR(bool rts, bool dtr) { if (tap_fp) { fprintf(tap_fp, "# %12.1f %11.3f rts=%d dtr=%d\n", tap_now_us() - tap_t0_us, PIC_FullIndex(), rts ? 1 : 0, dtr ? 1 : 0); fflush(tap_fp); } SERIAL_setRTS(comport,rts); SERIAL_setDTR(comport,dtr); } void CDirectSerial::setRTS(bool val) { if (tap_fp) { fprintf(tap_fp, "# %12.1f %11.3f rts=%d\n", tap_now_us() - tap_t0_us, PIC_FullIndex(), val ? 1 : 0); fflush(tap_fp); } SERIAL_setRTS(comport,val); } void CDirectSerial::setDTR(bool val) { if (tap_fp) { fprintf(tap_fp, "# %12.1f %11.3f dtr=%d\n", tap_now_us() - tap_t0_us, PIC_FullIndex(), val ? 1 : 0); fflush(tap_fp); } SERIAL_setDTR(comport,val); } #endif