Files
TeslaRel410/emulator/vpx-device/serialport/directserial.cpp
T
CydandClaude Fable 5 377047f37d RIO working: serial wire tap, conf tuning, four-patch writeup
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>
2026-07-04 00:22:14 -05:00

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