The RIO cockpit board now runs sustained sessions with button mashing; dropouts self-heal in ~1-3s. Documented in RIO-NOTES.md: - directserial RIO_TAP=<path> (host env): logs every TX/RX byte with host-us + emu-ms timestamps, plus config/RTS/DTR/break lines. This instrument found every root cause below. - Confs: rxburst:16 restored (no-burst reply pacing made the game ACK ~14ms late -> board dropped on the first long analog stream; the old 'rxburst corrupts boot' belief was the then-unpatched PCSPAK crash). priority=highest,highest (unfocused DOSBox was demoted and blew the ACK deadline). - BTL4OPT.EXE patch lineage (in ALPHA_1/, zip left pristine): v2 full DISABLE_AND_DIE NOPs (v1 left the IRQ/RTS-retract prologue live -> first protocol error deafened the driver), v3 TXMAXIDLE 4->32 (kills the button-press ACK-window livelock), v4 analog retry limit 15s->0.5s (dev value was 0.2s; recovery now near-instant). - Board firmware patch plan recorded for the EPROM-dump route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
445 lines
13 KiB
C++
445 lines
13 KiB
C++
/*
|
|
* 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 <chrono>
|
|
|
|
/* 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<double, std::micro>(
|
|
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 <n> 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=<path> (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
|