WinComm/drv/commdrv.c
2026-10-13 14:05:57 -05:00

1865 lines
59 KiB
C

// Part of KPCOMM, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// commdrv.c - KPCOMM.DRV -- High-speed COMM.DRV replacement
//
// Exported COMM.DRV entry points (Windows 3.1 DDAG signatures), port
// configuration, ring buffer management, and DLL entry/exit.
//
// Drop-in replacement for the Windows 3.1 COMM.DRV with proper 16550
// FIFO management for reliable operation at 57600 and 115200 baud.
#include "commdrv.h"
#include <stddef.h>
// -----------------------------------------------------------------------
// SYSTEM.INI [386Enh] keys, named COMn<suffix>
// -----------------------------------------------------------------------
#define INI_FILE "SYSTEM.INI"
#define INI_SECTION "386Enh"
#define INI_KEY_BASE "Base" // COMnBase: hex I/O address, used only if the BIOS has none
#define INI_KEY_BUFFER "Buffer" // COMnBuffer: minimum queue size
#define INI_KEY_FIFO "FIFO" // COMnFIFO: Boolean, 16550A FIFOs enabled by default
#define INI_KEY_IRQ "IRQ" // COMnIRQ: interrupt line
#define INI_KEY_KEEPDTR "KeepDTR" // COMnKeepDTR: Boolean, CloseComm leaves DTR up (default off)
#define INI_KEY_TRIGGER "RxTRIGGER" // COMnRxTRIGGER: RX FIFO trigger level in bytes (1/4/8/14)
#define INI_KEY_MAX 16 // "COMn" + longest suffix + NUL
#define INI_VALUE_MAX 16
#define COM_KEY_PREFIX "COM1"
#define COM_KEY_DIGIT 3 // Index of the port digit in COM_KEY_PREFIX
#define DEFAULT_TRIGGER 4 // COMnRxTRIGGER default (bytes): ~1 ms slack at 115200, 1/4 the interrupts of 1
#define HEX_PREFIX_LEN 2 // "0x"
#define HEX_DIGIT_BITS 4
// trmcom gives up when the transmitter makes no progress for this many BIOS ticks (~5 s)
#define DRAIN_TIMEOUT_TICKS 91
// Modem lines a DCB timeout can make output wait for (see waitTxLines)
#define TX_LINE_COUNT 3
typedef struct {
uint16_t timeoutOffset; // offsetof(DCB, <line>Timeout)
uint8_t msrBit; // Line state in the MSR
uint16_t error; // CE_* bit reported when the wait times out
} TxLineT;
#if DBG_ENABLED
#define DBG_LABEL_MAX 32 // Longest label dbgHex16 copies
#define DBG_LINE_MAX 48 // Label + ": 0x" + 4 hex digits + CRLF + NUL
#endif
// -----------------------------------------------------------------------
// Prototypes -- exported entry points
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export cclrbrk(int16_t commId);
uint16_t FAR * FAR PASCAL _export cevt(int16_t commId, int16_t evtMask);
uint16_t FAR PASCAL _export cevtget(int16_t commId, int16_t evtMask);
int32_t FAR PASCAL _export cextfcn(int16_t commId, int16_t func);
int16_t FAR PASCAL _export cflush(int16_t commId, int16_t queue);
int FAR PASCAL _export commNotifyWndProc(void);
int16_t FAR PASCAL _export commWriteString(int16_t commId, void FAR *buf, int16_t len);
int16_t FAR PASCAL _export csetbrk(int16_t commId);
uint16_t FAR PASCAL _export ctx(int16_t commId, int16_t ch);
int16_t FAR PASCAL _export enableNotification(int16_t commId, HWND hwnd, int16_t rxTrigger, int16_t txTrigger);
DCB FAR * FAR PASCAL _export getdcb(int16_t commId);
int16_t FAR PASCAL _export inicom(DCB FAR *dcb);
void FAR PASCAL _export reactivateOpenCommPorts(void);
int16_t FAR PASCAL _export readCommString(int16_t commId, void FAR *buf, int16_t len);
int16_t FAR PASCAL _export reccom(int16_t commId);
int16_t FAR PASCAL _export setcom(DCB FAR *dcb);
int16_t FAR PASCAL _export setque(int16_t commId, QdbT FAR *qdb);
uint16_t FAR PASCAL _export sndcom(int16_t commId, int16_t ch);
uint16_t FAR PASCAL _export stacom(int16_t commId, COMSTAT FAR *stat);
void FAR PASCAL _export suspendOpenCommPorts(void);
int16_t FAR PASCAL _export trmcom(int16_t commId);
int FAR PASCAL LibMain(HANDLE hInstance, WORD wDataSeg, WORD wHeapSize, LPSTR lpCmdLine);
int FAR PASCAL _export WEP(int nParam);
// -----------------------------------------------------------------------
// Prototypes -- internal helpers (shared ones are declared in commdrv.h)
// -----------------------------------------------------------------------
static void addErrors(PortStateT *port, uint16_t errors);
static bool allocBuffer(BufferT FAR *buf, uint16_t size);
static int16_t applyDcb(PortStateT *port, const DCB FAR *dcb, bool opening);
static uint16_t baudDivisor(uint16_t baud);
static uint16_t biosDataSelector(void);
static uint32_t cbrToBaud(uint16_t cbr);
static void clearUartStatus(uint16_t base);
static void closePort(PortStateT *port);
static int32_t comBaseIrq(int16_t commId);
static void comIniKey(char FAR *key, int16_t commId, const char FAR *suffix);
static int16_t dcbToLine(const DCB FAR *dcb, uint16_t FAR *divisor, uint8_t FAR *lcr);
static bool detect16550(uint16_t base);
static bool drainTx(PortStateT *port);
static void enableFifo(PortStateT *port);
static void freeBuffer(BufferT FAR *buf);
static PortStateT *getOpenPort(int16_t commId);
static int16_t hexDigit(char ch);
static void initPortState(PortStateT *port, int16_t commId, uint16_t base, uint8_t irq);
static uint16_t queueSize(int16_t commId, uint16_t requested);
static uint16_t readComIni(int16_t commId, const char FAR *suffix, uint16_t defVal);
static bool readComIniBool(int16_t commId, const char FAR *suffix, bool defVal);
static uint16_t readComIniHex(int16_t commId, const char FAR *suffix, uint16_t defVal);
static void readComIniString(int16_t commId, const char FAR *suffix, char FAR *value);
static void readPortConfig(int16_t commId, uint16_t FAR *base, uint8_t FAR *irq);
static uint16_t readRx(PortStateT *port, uint8_t FAR *dst, uint16_t len);
static void resetRx(PortStateT *port);
static void resetTx(PortStateT *port);
static bool resolvePostMessage(void);
static uint8_t rxTriggerBits(int16_t commId);
static int16_t setBreak(int16_t commId, bool on);
static int16_t triggerCode(uint16_t bytes);
static bool uartPresent(uint16_t base);
static uint16_t waitTxLines(PortStateT *port);
static void writeLine(PortStateT *port, uint16_t divisor, uint8_t lcr);
static uint16_t writeTx(PortStateT *port, const uint8_t FAR *src, uint16_t len);
// -----------------------------------------------------------------------
// Default port resources, used when neither the BIOS nor SYSTEM.INI says
// -----------------------------------------------------------------------
static const uint16_t defaultBase[MAX_PORTS] = {
COM1_BASE, COM2_BASE, COM3_BASE, COM4_BASE
};
static const uint8_t defaultIrq[MAX_PORTS] = {
COM1_IRQ, COM2_IRQ, COM3_IRQ, COM4_IRQ
};
// RX FIFO trigger level in bytes, indexed by the FCR trigger code
static const uint8_t fifoTriggerBytes[] = { 1, 4, 8, 14 };
static const TxLineT txLines[TX_LINE_COUNT] = {
{ offsetof(DCB, CtsTimeout), MSR_CTS, CE_CTSTO },
{ offsetof(DCB, DsrTimeout), MSR_DSR, CE_DSRTO },
{ offsetof(DCB, RlsTimeout), MSR_DCD, CE_RLSDTO }
};
// -----------------------------------------------------------------------
// Global state
// -----------------------------------------------------------------------
PortStateT ports[MAX_PORTS]; // One per COM port, indexed by cid; shared with the ISR
volatile bool portsSuspended = false; // Set while a DOS application owns the hardware
static uint16_t biosSelector = 0; // DPMI selector for segment 0040h (0 = not created yet)
#ifdef __WATCOMC__
// -----------------------------------------------------------------------
// Open Watcom's notice
//
// Open Watcom's license (the Sybase Open Watcom Public License, section
// 2.2(d)) asks for a notice, in the driver and in its documentation, that
// the source of the Open Watcom code it carries (its DLL start-up and C
// library helpers) is available, and where. This is the driver's copy,
// never shown; it is not static, so the compiler keeps it although nothing
// reads it. README.md gives the same text for the documentation. The
// MSVC build carries none of Open Watcom's code.
// -----------------------------------------------------------------------
const char owplNotice[] = "This software contains code from Open Watcom. Its source code is available under the terms of the Sybase Open Watcom Public License version 1.0 from https://github.com/open-watcom/open-watcom-v2";
#endif
// -----------------------------------------------------------------------
// addErrors - Record CE_* bits from application context
//
// The ISR also updates errorFlags, so the read-modify-write is guarded.
// -----------------------------------------------------------------------
static void addErrors(PortStateT *port, uint16_t errors) {
_disable();
port->errorFlags |= errors;
_enable();
}
// -----------------------------------------------------------------------
// allocBuffer - Allocate a fixed, page-locked ring buffer
//
// GMEM_FIXED keeps the address stable and GlobalPageLock keeps it
// resident in enhanced mode, so the ISR can never fault on it.
// GMEM_SHARE makes the driver own it rather than the calling task, so an
// application that dies without CloseComm cannot free memory the ISR is
// still filling. Never call with interrupts disabled: the global heap may
// need compaction or DPMI.
// -----------------------------------------------------------------------
static bool allocBuffer(BufferT FAR *buf, uint16_t size) {
HGLOBAL handle;
handle = GlobalAlloc(GMEM_FIXED | GMEM_SHARE, (DWORD)size);
if (!handle) {
return false;
}
GlobalPageLock(handle);
buf->handle = handle;
buf->data = (uint8_t FAR *)GlobalLock(handle);
return true;
}
// -----------------------------------------------------------------------
// applyDcb - Validate a DCB and make it the port's live configuration
//
// Shared by inicom and setcom. Throttling done under the old settings is
// released first, then line settings, DTR/RTS, transmit holds, and receive
// throttling are re-derived from the new DCB. fDtrDisable/fRtsDisable
// drive the lines at open and when they change, so an unrelated SetCommState
// does not undo an EscapeCommFunction. Returns 0 or an IE_* code.
// -----------------------------------------------------------------------
static int16_t applyDcb(PortStateT *port, const DCB FAR *dcb, bool opening) {
uint16_t divisor;
uint8_t lcr;
int16_t err;
bool dtrChanged;
bool rtsChanged;
err = dcbToLine(dcb, &divisor, &lcr);
if (err != 0) {
return err;
}
dtrChanged = opening || dcb->fDtrDisable != port->dcb.fDtrDisable;
rtsChanged = opening || dcb->fRtsDisable != port->dcb.fRtsDisable;
_disable();
releaseRxFlow(port);
_fmemcpy(&port->dcb, dcb, sizeof(DCB));
if (dtrChanged) {
port->dtrOn = !dcb->fDtrDisable;
}
if (rtsChanged) {
port->rtsOn = !dcb->fRtsDisable;
}
writeLine(port, divisor, lcr);
port->comDeb.baudRate = dcb->BaudRate;
if (!dcb->fOutX) {
port->txHold = (uint8_t)(port->txHold & ~TX_HOLD_XOFF);
}
writeMcr(port);
updateTxHold(port);
updateRxFlow(port);
_enable();
return 0;
}
// -----------------------------------------------------------------------
// baudDivisor - Convert a DCB BaudRate value to a UART divisor
//
// A high byte of 0xFF selects a CBR_* index; anything else is a raw rate
// (DDAG). Returns 0 for unsupported rates. The division truncates on
// purpose: raw values 57601-65279 give divisor 1 (115200 baud), which is
// how a 16-bit BaudRate can ask for 115200 (BuildCommDCB has been seen to
// store 0xE101 = 57601 for it).
// -----------------------------------------------------------------------
static uint16_t baudDivisor(uint16_t baud) {
uint32_t rate;
if ((baud & CBR_INDEX_MASK) == CBR_INDEX_MASK) {
rate = cbrToBaud(baud);
} else {
rate = baud;
}
if (rate < MIN_RAW_BAUD) {
return 0;
}
return (uint16_t)(UART_CLOCK_DIV / rate);
}
// -----------------------------------------------------------------------
// biosDataSelector - Selector for the BIOS data area (segment 0040h)
//
// Cached after the first DPMI call. Returns 0 if DPMI refuses.
// -----------------------------------------------------------------------
static uint16_t biosDataSelector(void) {
uint16_t sel;
int16_t failed;
if (biosSelector == 0) {
_asm {
mov ax, DPMI_SEG_TO_DESC
mov bx, BIOS_DATA_SEG
int DPMI_INT
mov sel, ax
sbb ax, ax
mov failed, ax
}
if (!failed) {
biosSelector = sel;
}
}
return biosSelector;
}
// -----------------------------------------------------------------------
// cbrToBaud - Translate a CBR_* index to a rate (0 = unsupported)
//
// CBR_128000 and CBR_256000 are beyond a 1.8432 MHz UART and are rejected.
// -----------------------------------------------------------------------
static uint32_t cbrToBaud(uint16_t cbr) {
switch (cbr) {
case CBR_110:
return 110UL;
case CBR_300:
return 300UL;
case CBR_600:
return 600UL;
case CBR_1200:
return 1200UL;
case CBR_2400:
return 2400UL;
case CBR_4800:
return 4800UL;
case CBR_9600:
return 9600UL;
case CBR_14400:
return 14400UL;
case CBR_19200:
return 19200UL;
case CBR_38400:
return 38400UL;
case CBR_56000:
return 56000UL;
case KPCBR_115200:
return 115200UL;
default:
return 0UL;
}
}
// -----------------------------------------------------------------------
// cclrbrk - Clear break (ordinal 14)
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export cclrbrk(int16_t commId) {
return setBreak(commId, false);
}
// -----------------------------------------------------------------------
// cevt - Set the event mask; return a pointer to the event word (ordinal 11)
//
// Only events enabled here are recorded in the event word (DDAG).
// -----------------------------------------------------------------------
uint16_t FAR * FAR PASCAL _export cevt(int16_t commId, int16_t evtMask) {
PortStateT *port;
LptStateT *lpt;
lpt = getOpenLpt(commId);
if (lpt) {
lpt->evtMask = (uint16_t)evtMask;
return (uint16_t FAR *)&lpt->evtWord;
}
port = getOpenPort(commId);
if (!port) {
return NULL;
}
port->comDeb.evtMask = (uint16_t)evtMask;
return (uint16_t FAR *)&port->comDeb.evtWord;
}
// -----------------------------------------------------------------------
// cevtget - Return and clear the requested event bits (ordinal 12)
// -----------------------------------------------------------------------
uint16_t FAR PASCAL _export cevtget(int16_t commId, int16_t evtMask) {
PortStateT *port;
LptStateT *lpt;
uint16_t events;
lpt = getOpenLpt(commId);
if (lpt) {
events = lpt->evtWord & (uint16_t)evtMask;
lpt->evtWord &= ~(uint16_t)evtMask;
return events;
}
port = getOpenPort(commId);
if (!port) {
return 0;
}
_disable();
events = port->comDeb.evtWord & (uint16_t)evtMask;
port->comDeb.evtWord &= ~(uint16_t)evtMask;
refreshNotify(port, CN_EVENT);
_enable();
return events;
}
// -----------------------------------------------------------------------
// cextfcn - Extended functions / EscapeCommFunction (ordinal 9)
//
// Returns 0 on success (16-bit API), a negative value for an unknown
// function or port, or the requested value for the GET* queries. Also
// accepts the KPCOMM extensions KPESC_KEEP_DTR_ON_CLOSE and
// KPESC_DROP_DTR_ON_CLOSE (see commdrv.h).
// -----------------------------------------------------------------------
int32_t FAR PASCAL _export cextfcn(int16_t commId, int16_t func) {
PortStateT *port;
LptStateT *lpt;
int32_t result;
// Queries that do not need an open port
switch (func) {
case GETMAXCOM:
return MAX_PORTS - 1;
case GETMAXLPT:
return lptMaxId();
case GETBASEIRQ:
return isLptId(commId) ? lptBaseIrq(commId) : comBaseIrq(commId);
}
lpt = getOpenLpt(commId);
if (lpt) {
if (func == RESETDEV) {
lptReset(lpt);
return 0;
}
return DRV_ERROR;
}
port = getOpenPort(commId);
if (!port) {
return DRV_ERROR;
}
result = 0;
_disable();
switch (func) {
case SETXOFF:
port->txHold = (uint8_t)(port->txHold | TX_HOLD_XOFF);
break;
case SETXON:
port->txHold = (uint8_t)(port->txHold & ~TX_HOLD_XOFF);
startTx(port);
break;
case SETRTS:
port->rtsOn = true;
writeMcr(port);
break;
case CLRRTS:
port->rtsOn = false;
writeMcr(port);
break;
case SETDTR:
port->dtrOn = true;
writeMcr(port);
break;
case CLRDTR:
port->dtrOn = false;
writeMcr(port);
break;
case RESETDEV:
// Printer reset; nothing to do for a serial port
break;
case KPESC_KEEP_DTR_ON_CLOSE:
port->keepDtrOnClose = true;
break;
case KPESC_DROP_DTR_ON_CLOSE:
port->keepDtrOnClose = false;
break;
default:
result = DRV_ERROR;
break;
}
_enable();
return result;
}
// -----------------------------------------------------------------------
// cflush - Discard the transmit (0) or receive (1) queue (ordinal 10)
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export cflush(int16_t commId, int16_t queue) {
PortStateT *port;
if (getOpenLpt(commId)) {
return 0;
}
port = getOpenPort(commId);
if (!port || (queue != FLUSH_TX && queue != FLUSH_RX)) {
return DRV_ERROR;
}
_disable();
if (queue == FLUSH_RX) {
resetRx(port);
if (port->useFifo) {
_outp(port->comDeb.baseAddr + UART_FCR, FCR_ENABLE | FCR_RX_RESET | port->comDeb.fifoTrigger);
}
} else {
resetTx(port);
if (port->useFifo) {
_outp(port->comDeb.baseAddr + UART_FCR, FCR_ENABLE | FCR_TX_RESET | port->comDeb.fifoTrigger);
}
}
refreshNotify(port, (uint8_t)((queue == FLUSH_RX) ? CN_RECEIVE : 0));
_enable();
return 0;
}
// -----------------------------------------------------------------------
// clearUartStatus - Read every UART status source so nothing is pending
// -----------------------------------------------------------------------
static void clearUartStatus(uint16_t base) {
(void)_inp(base + UART_LSR);
(void)_inp(base + UART_MSR);
(void)_inp(base + UART_RBR);
(void)_inp(base + UART_IIR);
}
// -----------------------------------------------------------------------
// closePort - Release a COM port's hardware, IRQ, and buffers
//
// Does not wait for output; trmcom drains first. Drops RTS and OUT2, and
// DTR as well unless keepDtrOnClose asks for the connection to survive
// (then a modem stays online for the next program, and with RTS/CTS flow
// control it holds incoming data until RTS is raised again).
// -----------------------------------------------------------------------
static void closePort(PortStateT *port) {
uint16_t base;
base = port->comDeb.baseAddr;
_disable();
port->isOpen = false;
writeIer(port, 0);
_enable();
if (port->useFifo) {
_outp(base + UART_FCR, FCR_ENABLE | FCR_RX_RESET | FCR_TX_RESET);
_outp(base + UART_FCR, 0);
}
_outp(base + UART_MCR, (port->keepDtrOnClose && port->dtrOn) ? MCR_DTR : 0);
clearUartStatus(base);
unhookIsr(port);
freeBuffer(&port->rxBuf);
freeBuffer(&port->txBuf);
}
// -----------------------------------------------------------------------
// comBaseIrq - GETBASEIRQ for a COM port: base in the low word, IRQ in
// the high word
// -----------------------------------------------------------------------
static int32_t comBaseIrq(int16_t commId) {
uint16_t base;
uint8_t irq;
if (commId < 0 || commId >= MAX_PORTS) {
return BASEIRQ_NO_PORT;
}
if (ports[commId].isOpen) {
base = ports[commId].comDeb.baseAddr;
irq = ports[commId].irq;
} else {
readPortConfig(commId, &base, &irq);
}
return MAKELONG(base, irq);
}
// -----------------------------------------------------------------------
// comIniKey - Build "COMn<suffix>" for a port
// -----------------------------------------------------------------------
static void comIniKey(char FAR *key, int16_t commId, const char FAR *suffix) {
lstrcpy(key, COM_KEY_PREFIX);
key[COM_KEY_DIGIT] = (char)(key[COM_KEY_DIGIT] + commId);
lstrcat(key, suffix);
}
// -----------------------------------------------------------------------
// commNotifyWndProc - Ordinal 101 stub
//
// The stock COMM.DRV and CyberCom export an undocumented function at
// ordinal 101; CyberCom's is a plain RETF with no parameters.
// -----------------------------------------------------------------------
int FAR PASCAL _export commNotifyWndProc(void) {
return 0;
}
// -----------------------------------------------------------------------
// commWriteString - Queue a block of bytes (ordinal 19)
//
// Returns the number of bytes queued; CE_TXFULL is set if not all fit.
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export commWriteString(int16_t commId, void FAR *buf, int16_t len) {
PortStateT *port;
LptStateT *lpt;
lpt = getOpenLpt(commId);
if (lpt) {
return lptWrite(lpt, (const uint8_t FAR *)buf, len);
}
port = getOpenPort(commId);
if (!port) {
return DRV_ERROR;
}
if (len <= 0) {
return 0;
}
return (int16_t)writeTx(port, (const uint8_t FAR *)buf, (uint16_t)len);
}
// -----------------------------------------------------------------------
// csetbrk - Set break (ordinal 13)
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export csetbrk(int16_t commId) {
return setBreak(commId, true);
}
// -----------------------------------------------------------------------
// ctx - Send one character ahead of the queue (ordinal 6)
//
// Returns CTX_BUSY if the previous one has not gone out yet (DDAG), or if
// a modem line guarded by a finite DCB timeout stayed low (CE_*TO set).
// -----------------------------------------------------------------------
uint16_t FAR PASCAL _export ctx(int16_t commId, int16_t ch) {
PortStateT *port;
LptStateT *lpt;
uint16_t result;
uint16_t lineErrors;
uint8_t data;
data = (uint8_t)ch;
lpt = getOpenLpt(commId);
if (lpt) {
return (lptWrite(lpt, &data, 1) == 1) ? 0 : CTX_BUSY;
}
port = getOpenPort(commId);
if (!port) {
return CE_MODE;
}
lineErrors = waitTxLines(port);
if (lineErrors) {
addErrors(port, lineErrors);
return CTX_BUSY;
}
_disable();
if (port->txImmediate != NO_CHAR) {
result = CTX_BUSY;
} else {
port->txImmediate = data;
startTx(port);
result = 0;
}
_enable();
return result;
}
#if DBG_ENABLED
// -----------------------------------------------------------------------
// dbgHex16 - Log "label: 0xNNNN"
// -----------------------------------------------------------------------
void dbgHex16(const char FAR *label, uint16_t val) {
static const char hex[] = "0123456789ABCDEF";
char buf[DBG_LINE_MAX];
int16_t i;
int16_t shift;
for (i = 0; i < DBG_LABEL_MAX && label[i]; i++) {
buf[i] = label[i];
}
buf[i++] = ':';
buf[i++] = ' ';
buf[i++] = '0';
buf[i++] = 'x';
for (shift = 12; shift >= 0; shift -= HEX_DIGIT_BITS) {
buf[i++] = hex[(val >> shift) & 0xF];
}
buf[i++] = '\r';
buf[i++] = '\n';
buf[i] = '\0';
OutputDebugString(buf);
}
// -----------------------------------------------------------------------
// dbgStr - Log a string
// -----------------------------------------------------------------------
void dbgStr(const char FAR *msg) {
OutputDebugString(msg);
}
#endif
// -----------------------------------------------------------------------
// dcbToLine - Validate a DCB's line settings and compute divisor and LCR
//
// Returns 0 or IE_BAUDRATE / IE_BYTESIZE / IE_DEFAULT. LCR_STB gives
// 1.5 stop bits with 5 data bits and 2 otherwise.
// -----------------------------------------------------------------------
static int16_t dcbToLine(const DCB FAR *dcb, uint16_t FAR *divisor, uint8_t FAR *lcr) {
uint8_t value;
*divisor = baudDivisor(dcb->BaudRate);
if (*divisor == 0) {
return IE_BAUDRATE;
}
if (dcb->ByteSize < MIN_BYTE_SIZE || dcb->ByteSize > MAX_BYTE_SIZE) {
return IE_BYTESIZE;
}
value = (uint8_t)(dcb->ByteSize - MIN_BYTE_SIZE);
switch (dcb->StopBits) {
case ONESTOPBIT:
break;
case ONE5STOPBITS:
case TWOSTOPBITS:
value |= LCR_STB;
break;
default:
return IE_DEFAULT;
}
switch (dcb->Parity) {
case NOPARITY:
break;
case ODDPARITY:
value |= LCR_PEN;
break;
case EVENPARITY:
value |= LCR_PEN | LCR_EPS;
break;
case MARKPARITY:
value |= LCR_PEN | LCR_SPAR;
break;
case SPACEPARITY:
value |= LCR_PEN | LCR_EPS | LCR_SPAR;
break;
default:
return IE_DEFAULT;
}
*lcr = value;
return 0;
}
// -----------------------------------------------------------------------
// detect16550 - True if the UART is a 16550A with working FIFOs
//
// Leaves the FIFOs disabled; enableFifo turns them on with the trigger.
// -----------------------------------------------------------------------
static bool detect16550(uint16_t base) {
uint8_t iir;
_outp(base + UART_FCR, FCR_ENABLE);
iir = (uint8_t)_inp(base + UART_IIR);
_outp(base + UART_FCR, 0);
// 16550A sets both IIR bits 7:6; the original 16550 (broken FIFO) only bit 7
return (iir & IIR_FIFO_MASK) == IIR_FIFO_MASK;
}
// -----------------------------------------------------------------------
// drainTx - Wait for queued output to leave the UART (for trmcom)
//
// Fails at once if a modem line guarded by a finite DCB timeout stays low
// past it; otherwise gives up after DRAIN_TIMEOUT_TICKS without progress,
// e.g. when held by XOFF or flow control. Counts BIOS tick changes rather
// than comparing tick values, so midnight rollover cannot cause a false
// timeout. GetTickCount lives in USER, which COMM.DRV cannot import.
// Returns false on timeout.
// -----------------------------------------------------------------------
static bool drainTx(PortStateT *port) {
uint16_t base;
uint16_t lastCount;
uint16_t lastTick;
uint16_t idleTicks;
uint16_t count;
uint16_t tick;
if (biosDataSelector() == 0) {
return port->comDeb.txCount == 0;
}
if (port->comDeb.txCount > 0 && waitTxLines(port) != 0) {
return false;
}
base = port->comDeb.baseAddr;
lastCount = port->comDeb.txCount;
lastTick = readBiosWord(BDA_TICK_COUNT);
idleTicks = 0;
for (;;) {
count = port->comDeb.txCount;
if (count == 0 && port->flowChar == NO_CHAR && port->txImmediate == NO_CHAR && (_inp(base + UART_LSR) & LSR_TEMT)) {
return true;
}
if (count != lastCount) {
lastCount = count;
idleTicks = 0;
}
tick = readBiosWord(BDA_TICK_COUNT);
if (tick != lastTick) {
lastTick = tick;
idleTicks++;
if (idleTicks > DRAIN_TIMEOUT_TICKS) {
return false;
}
}
}
}
// -----------------------------------------------------------------------
// enableFifo - Enable and reset the FIFOs with the configured RX trigger
// -----------------------------------------------------------------------
static void enableFifo(PortStateT *port) {
if (port->useFifo) {
_outp(port->comDeb.baseAddr + UART_FCR, FCR_ENABLE | FCR_RX_RESET | FCR_TX_RESET | port->comDeb.fifoTrigger);
}
}
// -----------------------------------------------------------------------
// enableNotification - Register a window for WM_COMMNOTIFY (ordinal 100)
//
// hwnd NULL disables notifications; a trigger of -1 disables that kind.
// Posts at once if a condition already holds. Returns TRUE/FALSE.
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export enableNotification(int16_t commId, HWND hwnd, int16_t rxTrigger, int16_t txTrigger) {
PortStateT *port;
port = getOpenPort(commId);
if (!port) {
return FALSE;
}
if (hwnd && !resolvePostMessage()) {
return FALSE;
}
_disable();
port->hwndNotify = hwnd;
port->rxNotifyTrigger = rxTrigger;
port->txNotifyTrigger = txTrigger;
port->notifySent = 0;
checkNotify(port);
_enable();
return TRUE;
}
// -----------------------------------------------------------------------
// freeBuffer - Release a buffer from allocBuffer (no-op if empty)
// -----------------------------------------------------------------------
static void freeBuffer(BufferT FAR *buf) {
if (!buf->handle) {
return;
}
GlobalPageUnlock(buf->handle);
GlobalUnlock(buf->handle);
GlobalFree(buf->handle);
buf->handle = NULL;
buf->data = NULL;
}
// -----------------------------------------------------------------------
// getdcb - Return a pointer to the port's DCB (ordinal 15)
// -----------------------------------------------------------------------
DCB FAR * FAR PASCAL _export getdcb(int16_t commId) {
PortStateT *port;
LptStateT *lpt;
lpt = getOpenLpt(commId);
if (lpt) {
return &lpt->dcb;
}
port = getOpenPort(commId);
if (!port) {
return NULL;
}
return &port->dcb;
}
// -----------------------------------------------------------------------
// getOpenPort - Port state for an open COM cid, or NULL
// -----------------------------------------------------------------------
static PortStateT *getOpenPort(int16_t commId) {
if (commId < 0 || commId >= MAX_PORTS || !ports[commId].isOpen) {
return NULL;
}
return &ports[commId];
}
// -----------------------------------------------------------------------
// hexDigit - Value of a hex digit, or -1
// -----------------------------------------------------------------------
static int16_t hexDigit(char ch) {
if (ch >= '0' && ch <= '9') {
return ch - '0';
}
if (ch >= 'A' && ch <= 'F') {
return ch - 'A' + 10;
}
if (ch >= 'a' && ch <= 'f') {
return ch - 'a' + 10;
}
return -1;
}
// -----------------------------------------------------------------------
// inicom - Open a port (ordinal 1)
//
// The cid is dcb->Id (0-3 = COM1-COM4, 0x80-0x82 = LPT1-LPT3). Settings
// are validated before any hardware is touched. The queues get a default
// size here; USER calls setque right after with the application's sizes.
// Returns 0 or an IE_* code (DDAG).
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export inicom(DCB FAR *dcb) {
PortStateT *port;
int16_t commId;
int16_t err;
uint16_t base;
uint16_t divisor;
uint16_t size;
uint8_t irq;
uint8_t lcr;
if (!dcb) {
return IE_DEFAULT;
}
commId = dcb->Id;
dbgHex16("KPCOMM: inicom id", (uint16_t)commId);
dbgHex16("KPCOMM: inicom CtsTimeout", dcb->CtsTimeout);
dbgHex16("KPCOMM: inicom DsrTimeout", dcb->DsrTimeout);
dbgHex16("KPCOMM: inicom RlsTimeout", dcb->RlsTimeout);
if (isLptId(commId)) {
return lptOpen(dcb);
}
if (commId >= MAX_PORTS) {
return IE_BADID;
}
port = &ports[commId];
if (port->isOpen) {
return IE_OPEN;
}
err = dcbToLine(dcb, &divisor, &lcr);
if (err != 0) {
return err;
}
readPortConfig(commId, &base, &irq);
if (!uartPresent(base)) {
return IE_HARDWARE;
}
initPortState(port, commId, base, irq);
size = queueSize(commId, DEFAULT_QUEUE_SIZE);
if (!allocBuffer(&port->rxBuf, size) || !allocBuffer(&port->txBuf, size)) {
freeBuffer(&port->rxBuf);
return IE_MEMORY;
}
port->comDeb.rxSize = size;
port->comDeb.txSize = size;
// Quiesce the UART before the IRQ is unmasked: a condition left over
// from an earlier session would otherwise interrupt before the port is
// marked open and could never be cleared.
_outp(base + UART_IER, 0);
clearUartStatus(base);
if (hookIsr(port) != 0) {
freeBuffer(&port->rxBuf);
freeBuffer(&port->txBuf);
return IE_HARDWARE;
}
enableFifo(port);
port->comDeb.msrShadow = (uint8_t)_inp(base + UART_MSR);
port->isOpen = true;
(void)applyDcb(port, dcb, true);
// Raising DTR/RTS may have changed the modem lines (RTS loops back to
// CTS on a null-modem cable). Take a fresh MSR snapshot in the same
// guarded step that enables interrupts so no transition is missed.
_disable();
clearUartStatus(base);
port->comDeb.msrShadow = (uint8_t)_inp(base + UART_MSR);
writeIer(port, (uint8_t)(port->comDeb.ierShadow | IER_RX));
updateTxHold(port);
_enable();
dbgStr("KPCOMM: inicom OK\r\n");
return 0;
}
// -----------------------------------------------------------------------
// initPortState - Reset a port's state for a fresh open
// -----------------------------------------------------------------------
static void initPortState(PortStateT *port, int16_t commId, uint16_t base, uint8_t irq) {
_fmemset(port, 0, sizeof(PortStateT));
port->comDeb.baseAddr = base;
port->comDeb.fifoTrigger = rxTriggerBits(commId);
port->irq = irq;
port->useFifo = detect16550(base) && readComIniBool(commId, INI_KEY_FIFO, true);
port->keepDtrOnClose = readComIniBool(commId, INI_KEY_KEEPDTR, false);
port->flowChar = NO_CHAR;
port->txImmediate = NO_CHAR;
port->rxNotifyTrigger = NOTIFY_OFF;
port->txNotifyTrigger = NOTIFY_OFF;
}
// -----------------------------------------------------------------------
// queueSize - Queue size for a request, raised to the COMnBuffer floor
// -----------------------------------------------------------------------
static uint16_t queueSize(int16_t commId, uint16_t requested) {
uint16_t minSize;
minSize = readComIni(commId, INI_KEY_BUFFER, MIN_QUEUE_SIZE);
if (minSize < MIN_QUEUE_SIZE) {
minSize = MIN_QUEUE_SIZE;
}
return (requested > minSize) ? requested : minSize;
}
// -----------------------------------------------------------------------
// reactivateOpenCommPorts - Take the hardware back after a DOS application
// (ordinal 18)
//
// The DOS application may have reprogrammed everything, so line settings,
// FIFOs, modem lines, interrupt enables, and PIC masks are all restored
// from the saved state. Every UART is silenced and reprogrammed before
// the ISR takes the ports back, so it never services a UART still in the
// DOS application's configuration. Queued data is kept.
// -----------------------------------------------------------------------
void FAR PASCAL _export reactivateOpenCommPorts(void) {
PortStateT *port;
int16_t i;
dbgStr("KPCOMM: reactivate\r\n");
for (i = 0; i < MAX_PORTS; i++) {
port = &ports[i];
if (port->isOpen) {
_disable();
writeIer(port, 0);
writeLine(port, baudDivisor(port->dcb.BaudRate), port->comDeb.lcrShadow);
enableFifo(port);
writeMcr(port);
_enable();
}
}
portsSuspended = false;
for (i = 0; i < MAX_PORTS; i++) {
port = &ports[i];
if (port->isOpen) {
_disable();
clearUartStatus(port->comDeb.baseAddr);
port->comDeb.msrShadow = (uint8_t)_inp(port->comDeb.baseAddr + UART_MSR);
writeIer(port, IER_RX);
updateTxHold(port);
_enable();
}
}
reactivateIrqs();
}
// -----------------------------------------------------------------------
// readBiosWord - Read a word from the BIOS data area (0 if unavailable)
// -----------------------------------------------------------------------
uint16_t readBiosWord(uint16_t offset) {
uint16_t sel;
sel = biosDataSelector();
if (sel == 0) {
return 0;
}
return *(volatile uint16_t FAR *)MAKELP(sel, offset);
}
// -----------------------------------------------------------------------
// readComIni - Read COMn<suffix> from SYSTEM.INI [386Enh] as an integer
// -----------------------------------------------------------------------
static uint16_t readComIni(int16_t commId, const char FAR *suffix, uint16_t defVal) {
char key[INI_KEY_MAX];
comIniKey(key, commId, suffix);
return (uint16_t)GetPrivateProfileInt(INI_SECTION, key, (int)defVal, INI_FILE);
}
// -----------------------------------------------------------------------
// readComIniBool - Read COMn<suffix> as a SYSTEM.INI Boolean
//
// Accepts 1-9/true/yes/on and 0/false/no/off (any case) by hand, since
// lstrcmpi lives in USER. Returns defVal when missing or unrecognized.
// -----------------------------------------------------------------------
static bool readComIniBool(int16_t commId, const char FAR *suffix, bool defVal) {
char value[INI_VALUE_MAX];
readComIniString(commId, suffix, value);
switch (value[0]) {
case 't':
case 'T':
case 'y':
case 'Y':
return true;
case '0':
case 'f':
case 'F':
case 'n':
case 'N':
return false;
case 'o':
case 'O':
return value[1] == 'n' || value[1] == 'N';
default:
return (value[0] >= '1' && value[0] <= '9') ? true : defVal;
}
}
// -----------------------------------------------------------------------
// readComIniHex - Read COMn<suffix> as hex ("3E8", "0x3E8", or "3E8h")
//
// Parsing stops at the first non-hex character. Returns defVal when the
// key is missing or holds no nonzero value.
// -----------------------------------------------------------------------
static uint16_t readComIniHex(int16_t commId, const char FAR *suffix, uint16_t defVal) {
char value[INI_VALUE_MAX];
uint16_t result;
int16_t digit;
int16_t i;
readComIniString(commId, suffix, value);
i = 0;
if (value[0] == '0' && (value[1] == 'x' || value[1] == 'X')) {
i = HEX_PREFIX_LEN;
}
result = 0;
for (;;) {
digit = hexDigit(value[i]);
if (digit < 0) {
break;
}
result = (uint16_t)((result << HEX_DIGIT_BITS) | (uint16_t)digit);
i++;
}
return (result != 0) ? result : defVal;
}
// -----------------------------------------------------------------------
// readComIniString - Read COMn<suffix> into value (INI_VALUE_MAX bytes;
// empty string if missing)
// -----------------------------------------------------------------------
static void readComIniString(int16_t commId, const char FAR *suffix, char FAR *value) {
char key[INI_KEY_MAX];
comIniKey(key, commId, suffix);
GetPrivateProfileString(INI_SECTION, key, "", value, INI_VALUE_MAX, INI_FILE);
}
// -----------------------------------------------------------------------
// readCommString - Read a block from the receive queue (ordinal 20)
//
// Returns the number of bytes read (0 if none).
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export readCommString(int16_t commId, void FAR *buf, int16_t len) {
PortStateT *port;
if (getOpenLpt(commId)) {
return 0;
}
port = getOpenPort(commId);
if (!port) {
return DRV_ERROR;
}
if (len <= 0) {
return 0;
}
return (int16_t)readRx(port, (uint8_t FAR *)buf, (uint16_t)len);
}
// -----------------------------------------------------------------------
// readPortConfig - Base address and IRQ for a COM port
//
// DDAG: the BIOS data area wins; COMnBase only fills a missing entry.
// COMnIRQ overrides the standard IRQ. "IRQ 2" means IRQ 9 on AT
// hardware. Out-of-range IRQs become 0, which hookIsr rejects.
// -----------------------------------------------------------------------
static void readPortConfig(int16_t commId, uint16_t FAR *base, uint8_t FAR *irq) {
uint16_t irqVal;
*base = readBiosWord(BDA_COM_BASES + commId * sizeof(uint16_t));
if (*base == 0) {
*base = readComIniHex(commId, INI_KEY_BASE, defaultBase[commId]);
}
irqVal = readComIni(commId, INI_KEY_IRQ, defaultIrq[commId]);
if (irqVal == IRQ_CASCADE) {
irqVal = IRQ_CASCADE_TARGET;
}
*irq = (irqVal < IRQ_COUNT) ? (uint8_t)irqVal : 0;
}
// -----------------------------------------------------------------------
// readRx - Move up to len bytes from the receive queue to dst
//
// Single consumer: only this side moves rxTail, and the ISR only grows
// rxCount, so the snapshot taken here stays valid. The copy runs with
// interrupts enabled; only the count update is guarded.
// -----------------------------------------------------------------------
static uint16_t readRx(PortStateT *port, uint8_t FAR *dst, uint16_t len) {
uint8_t FAR *buf;
uint16_t size;
uint16_t tail;
uint16_t avail;
uint16_t chunk;
avail = port->comDeb.rxCount;
if (avail > len) {
avail = len;
}
if (avail == 0) {
return 0;
}
buf = port->rxBuf.data;
size = port->comDeb.rxSize;
tail = port->comDeb.rxTail;
chunk = size - tail;
if (chunk > avail) {
chunk = avail;
}
_fmemcpy(dst, buf + tail, chunk);
if (chunk < avail) {
_fmemcpy(dst + chunk, buf, avail - chunk);
tail = avail - chunk;
} else {
// tail + chunk <= size, so this cannot overflow 16 bits
tail += chunk;
if (tail == size) {
tail = 0;
}
}
port->comDeb.rxTail = tail;
_disable();
port->comDeb.rxCount -= avail;
updateRxFlow(port);
refreshNotify(port, CN_RECEIVE);
_enable();
return avail;
}
// -----------------------------------------------------------------------
// reccom - Read one byte (ordinal 4)
//
// Returns the byte, RECCOM_NO_DATA (-2) if the queue is empty, or -1 on
// error (DDAG).
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export reccom(int16_t commId) {
PortStateT *port;
uint8_t data;
if (getOpenLpt(commId)) {
return RECCOM_NO_DATA;
}
port = getOpenPort(commId);
if (!port) {
return DRV_ERROR;
}
if (readRx(port, &data, 1) == 0) {
return RECCOM_NO_DATA;
}
return data;
}
// -----------------------------------------------------------------------
// resetRx - Empty the receive queue and release RX throttling
// (interrupts disabled)
// -----------------------------------------------------------------------
static void resetRx(PortStateT *port) {
port->comDeb.rxHead = 0;
port->comDeb.rxTail = 0;
port->comDeb.rxCount = 0;
port->eofSeen = false;
updateRxFlow(port);
}
// -----------------------------------------------------------------------
// resetTx - Empty the transmit queue (interrupts disabled)
// -----------------------------------------------------------------------
static void resetTx(PortStateT *port) {
port->comDeb.txHead = 0;
port->comDeb.txTail = 0;
port->comDeb.txCount = 0;
}
// -----------------------------------------------------------------------
// resolvePostMessage - Look up USER's PostMessage on first use
//
// COMM.DRV loads before USER, so it cannot import USER statically.
// -----------------------------------------------------------------------
static bool resolvePostMessage(void) {
HMODULE user;
if (!postMessageProc) {
user = GetModuleHandle("USER");
if (user) {
postMessageProc = (PostMessageProcT)GetProcAddress((HINSTANCE)user, "PostMessage");
}
}
return postMessageProc != NULL;
}
// -----------------------------------------------------------------------
// rxTriggerBits - FCR trigger bits for COMnRxTRIGGER (1, 4, 8, or 14 bytes)
//
// Values the 16550A cannot do fall back to DEFAULT_TRIGGER.
// -----------------------------------------------------------------------
static uint8_t rxTriggerBits(int16_t commId) {
int16_t code;
code = triggerCode(readComIni(commId, INI_KEY_TRIGGER, DEFAULT_TRIGGER));
if (code < 0) {
code = triggerCode(DEFAULT_TRIGGER);
}
return (uint8_t)(code << FCR_TRIG_SHIFT);
}
// -----------------------------------------------------------------------
// setBreak - Set or clear the break condition (csetbrk/cclrbrk)
// -----------------------------------------------------------------------
static int16_t setBreak(int16_t commId, bool on) {
PortStateT *port;
port = getOpenPort(commId);
if (!port) {
return DRV_ERROR;
}
if (on) {
port->comDeb.lcrShadow = (uint8_t)(port->comDeb.lcrShadow | LCR_SBRK);
} else {
port->comDeb.lcrShadow = (uint8_t)(port->comDeb.lcrShadow & ~LCR_SBRK);
}
_outp(port->comDeb.baseAddr + UART_LCR, port->comDeb.lcrShadow);
return 0;
}
// -----------------------------------------------------------------------
// setcom - Apply a new DCB (ordinal 2)
//
// Takes only the DCB (RETF 4); the cid is dcb->Id. Returns 0 or IE_*.
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export setcom(DCB FAR *dcb) {
LptStateT *lpt;
int16_t commId;
if (!dcb) {
return IE_DEFAULT;
}
commId = dcb->Id;
dbgHex16("KPCOMM: setcom baud", dcb->BaudRate);
lpt = getOpenLpt(commId);
if (lpt) {
_fmemcpy(&lpt->dcb, dcb, sizeof(DCB));
return 0;
}
if (commId >= MAX_PORTS && !isLptId(commId)) {
return IE_BADID;
}
if (!getOpenPort(commId)) {
return IE_NOPEN;
}
return applyDcb(&ports[commId], dcb, false);
}
// -----------------------------------------------------------------------
// setque - Size the receive and transmit queues (ordinal 3)
//
// Takes (cid, qdb FAR *) -- RETF 6. Only the requested sizes are used
// (raised to the COMnBuffer floor); USER's queue memory is ignored in
// favor of our own page-locked buffers. Buffers are replaced only if
// the size changes, and allocation happens before interrupts are
// disabled. Both queues are emptied. Returns 0 or IE_*.
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export setque(int16_t commId, QdbT FAR *qdb) {
PortStateT *port;
BufferT newRx;
BufferT newTx;
BufferT oldRx;
BufferT oldTx;
uint16_t rxSize;
uint16_t txSize;
if (isLptId(commId)) {
return getOpenLpt(commId) ? 0 : IE_NOPEN;
}
if (commId < 0 || commId >= MAX_PORTS) {
return IE_BADID;
}
port = &ports[commId];
if (!port->isOpen) {
return IE_NOPEN;
}
if (!qdb) {
return IE_DEFAULT;
}
rxSize = queueSize(commId, (uint16_t)qdb->rxSize);
txSize = queueSize(commId, (uint16_t)qdb->txSize);
dbgHex16("KPCOMM: setque rx", rxSize);
dbgHex16("KPCOMM: setque tx", txSize);
newRx = port->rxBuf;
newTx = port->txBuf;
if (rxSize != port->comDeb.rxSize && !allocBuffer(&newRx, rxSize)) {
return IE_MEMORY;
}
if (txSize != port->comDeb.txSize && !allocBuffer(&newTx, txSize)) {
if (newRx.handle != port->rxBuf.handle) {
freeBuffer(&newRx);
}
return IE_MEMORY;
}
_disable();
oldRx = port->rxBuf;
oldTx = port->txBuf;
port->rxBuf = newRx;
port->txBuf = newTx;
port->comDeb.rxSize = rxSize;
port->comDeb.txSize = txSize;
resetRx(port);
resetTx(port);
refreshNotify(port, CN_RECEIVE);
_enable();
if (oldRx.handle != newRx.handle) {
freeBuffer(&oldRx);
}
if (oldTx.handle != newTx.handle) {
freeBuffer(&oldTx);
}
return 0;
}
// -----------------------------------------------------------------------
// sndcom - Queue one byte (ordinal 5)
//
// Returns 0, or nonzero if it could not be queued (DDAG).
// -----------------------------------------------------------------------
uint16_t FAR PASCAL _export sndcom(int16_t commId, int16_t ch) {
PortStateT *port;
LptStateT *lpt;
uint8_t data;
data = (uint8_t)ch;
lpt = getOpenLpt(commId);
if (lpt) {
return (lptWrite(lpt, &data, 1) == 1) ? 0 : SNDCOM_FAILED;
}
port = getOpenPort(commId);
if (!port) {
return SNDCOM_FAILED;
}
return (writeTx(port, &data, 1) == 1) ? 0 : SNDCOM_FAILED;
}
// -----------------------------------------------------------------------
// stacom - Return and clear the error flags; fill COMSTAT (ordinal 8)
//
// Returns CE_MODE for an invalid cid.
// -----------------------------------------------------------------------
uint16_t FAR PASCAL _export stacom(int16_t commId, COMSTAT FAR *stat) {
PortStateT *port;
LptStateT *lpt;
uint16_t errors;
uint16_t inQue;
uint16_t outQue;
uint8_t status;
lpt = getOpenLpt(commId);
if (lpt) {
errors = lpt->errorFlags;
lpt->errorFlags = 0;
if (stat) {
_fmemset(stat, 0, sizeof(COMSTAT));
}
return errors;
}
port = getOpenPort(commId);
if (!port) {
return CE_MODE;
}
status = 0;
_disable();
errors = port->errorFlags;
port->errorFlags = 0;
if (port->txHold & TX_HOLD_CTS) {
status |= CSTF_CTSHOLD;
}
if (port->txHold & TX_HOLD_DSR) {
status |= CSTF_DSRHOLD;
}
if (port->txHold & TX_HOLD_RLSD) {
status |= CSTF_RLSDHOLD;
}
if (port->txHold & TX_HOLD_XOFF) {
status |= CSTF_XOFFHOLD;
}
if (port->rxThrottle & RX_THROTTLE_XOFF) {
status |= CSTF_XOFFSENT;
}
if (port->eofSeen) {
status |= CSTF_EOF;
}
if (port->txImmediate != NO_CHAR) {
status |= CSTF_TXIM;
}
inQue = port->comDeb.rxCount;
outQue = port->comDeb.txCount;
_enable();
if (stat) {
stat->status = status;
stat->cbInQue = inQue;
stat->cbOutQue = outQue;
}
return errors;
}
// -----------------------------------------------------------------------
// suspendOpenCommPorts - Hand the hardware to a DOS application
// (ordinal 17)
//
// Called by WINOLDAP. Our UARTs are silenced, and the ISR passes every
// interrupt on our IRQs to the previous handlers until reactivation. DTR
// and RTS are left alone so a modem connection survives the switch.
// -----------------------------------------------------------------------
void FAR PASCAL _export suspendOpenCommPorts(void) {
int16_t i;
dbgStr("KPCOMM: suspend\r\n");
for (i = 0; i < MAX_PORTS; i++) {
if (ports[i].isOpen) {
_disable();
writeIer(&ports[i], 0);
_enable();
}
}
portsSuspended = true;
}
// -----------------------------------------------------------------------
// triggerCode - FCR trigger code (0-3) for a trigger level in bytes, or -1
// -----------------------------------------------------------------------
static int16_t triggerCode(uint16_t bytes) {
int16_t code;
for (code = 0; code < (int16_t)sizeof(fifoTriggerBytes); code++) {
if (fifoTriggerBytes[code] == bytes) {
return code;
}
}
return -1;
}
// -----------------------------------------------------------------------
// trmcom - Close a port (ordinal 7)
//
// Transmits the output queue first (DDAG). DTR is dropped unless the port
// was told to keep it (COMnKeepDTR / KPESC_KEEP_DTR_ON_CLOSE). Returns 0,
// TRMCOM_TIMEOUT if the output could not be sent, or 0x8000 (CE_MODE) for
// an invalid cid.
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export trmcom(int16_t commId) {
PortStateT *port;
bool drained;
dbgHex16("KPCOMM: trmcom id", (uint16_t)commId);
if (isLptId(commId)) {
return lptClose(commId);
}
port = getOpenPort(commId);
if (!port) {
return (int16_t)CE_MODE;
}
drained = drainTx(port);
closePort(port);
return drained ? 0 : TRMCOM_TIMEOUT;
}
// -----------------------------------------------------------------------
// uartPresent - True if a UART answers at base
//
// LCR reads back what was written; an empty I/O address floats to 0xFF.
// -----------------------------------------------------------------------
static bool uartPresent(uint16_t base) {
_outp(base + UART_LCR, UART_PRESENT_LCR);
return (uint8_t)_inp(base + UART_LCR) == UART_PRESENT_LCR;
}
// -----------------------------------------------------------------------
// waitTxLines - Wait for the modem lines that finite DCB timeouts guard
//
// Cts/Dsr/RlsTimeout: IGNORE (0) skips the line, INFINITE makes it flow
// control (the ISR holds output through txHold), and anything else is how
// many milliseconds output waits for the line to go high (DDAG). Called
// before output is accepted, with interrupts enabled, watching the MSR
// shadow the ISR keeps current. The wait blocks the caller, as the stock
// driver's did. Returns the CE_CTSTO/CE_DSRTO/CE_RLSDTO bits of lines
// still low when their time ran out (0 = go ahead).
// -----------------------------------------------------------------------
static uint16_t waitTxLines(PortStateT *port) {
uint16_t elapsed;
uint16_t errors;
uint16_t timeout;
uint16_t lastTick;
uint16_t tick;
uint16_t i;
uint8_t msr;
bool waiting;
// Without the BIOS tick there is no clock: time out at once
elapsed = (biosDataSelector() != 0) ? 0 : UINT16_MAX;
lastTick = readBiosWord(BDA_TICK_COUNT);
for (;;) {
msr = port->comDeb.msrShadow;
errors = 0;
waiting = false;
for (i = 0; i < TX_LINE_COUNT; i++) {
timeout = *(const uint16_t *)((const uint8_t *)&port->dcb + txLines[i].timeoutOffset);
if (timeout == IGNORE || timeout == INFINITE || (msr & txLines[i].msrBit)) {
continue;
}
if (elapsed >= timeout) {
errors |= txLines[i].error;
} else {
waiting = true;
}
}
if (!waiting) {
return errors;
}
tick = readBiosWord(BDA_TICK_COUNT);
if (tick != lastTick) {
lastTick = tick;
elapsed = (elapsed < UINT16_MAX - MS_PER_BIOS_TICK) ? (uint16_t)(elapsed + MS_PER_BIOS_TICK) : UINT16_MAX;
}
}
}
// -----------------------------------------------------------------------
// writeLine - Program the divisor and LCR, keeping the break state
//
// Interrupts must be disabled: while DLAB is set, offsets 0/1 are the
// divisor latch and the ISR would read garbage from "RBR".
// -----------------------------------------------------------------------
static void writeLine(PortStateT *port, uint16_t divisor, uint8_t lcr) {
uint16_t base;
base = port->comDeb.baseAddr;
lcr = (uint8_t)((lcr & ~LCR_SBRK) | (port->comDeb.lcrShadow & LCR_SBRK));
_outp(base + UART_LCR, lcr | LCR_DLAB);
_outp(base + UART_DLL, LOBYTE(divisor));
_outp(base + UART_DLM, HIBYTE(divisor));
_outp(base + UART_LCR, lcr);
port->comDeb.lcrShadow = lcr;
}
// -----------------------------------------------------------------------
// writeTx - Copy up to len bytes into the transmit queue and start TX
//
// First waits for any modem lines a finite DCB timeout guards; if one
// stays low, nothing is queued and its CE_*TO bit is set. Single
// producer: only this side moves txHead, and the ISR only shrinks txCount,
// so the free space seen here can only grow. The copy runs with
// interrupts enabled; publishing the new count is the only guarded step.
// Sets CE_TXFULL if not everything fit. Returns the bytes queued.
// -----------------------------------------------------------------------
static uint16_t writeTx(PortStateT *port, const uint8_t FAR *src, uint16_t len) {
uint8_t FAR *buf;
uint16_t size;
uint16_t head;
uint16_t room;
uint16_t chunk;
uint16_t lineErrors;
lineErrors = waitTxLines(port);
if (lineErrors) {
addErrors(port, lineErrors);
return 0;
}
size = port->comDeb.txSize;
room = size - port->comDeb.txCount;
if (room > len) {
room = len;
}
if (room > 0) {
buf = port->txBuf.data;
head = port->comDeb.txHead;
chunk = size - head;
if (chunk > room) {
chunk = room;
}
_fmemcpy(buf + head, src, chunk);
if (chunk < room) {
_fmemcpy(buf, src + chunk, room - chunk);
head = room - chunk;
} else {
// head + chunk <= size, so this cannot overflow 16 bits
head += chunk;
if (head == size) {
head = 0;
}
}
port->comDeb.txHead = head;
}
_disable();
port->comDeb.txCount += room;
if (room < len) {
port->errorFlags |= CE_TXFULL;
}
startTx(port);
refreshNotify(port, 0);
_enable();
return room;
}
// -----------------------------------------------------------------------
// LibMain - DLL entry point
//
// Static data is zero-filled by the loader, which is the closed state for
// every port.
// -----------------------------------------------------------------------
int FAR PASCAL LibMain(HANDLE hInstance, WORD wDataSeg, WORD wHeapSize, LPSTR lpCmdLine) {
(void)hInstance;
(void)wDataSeg;
(void)wHeapSize;
(void)lpCmdLine;
dbgStr("KPCOMM: LibMain\r\n");
return 1;
}
// -----------------------------------------------------------------------
// WEP - DLL exit procedure
//
// Releases hardware without waiting for output: Windows is going away.
// Ports set to keep DTR leave it up, so a modem connection can outlive
// Windows itself.
// -----------------------------------------------------------------------
int FAR PASCAL _export WEP(int nParam) {
int16_t i;
(void)nParam;
for (i = 0; i < MAX_PORTS; i++) {
if (ports[i].isOpen) {
closePort(&ports[i]);
}
}
lptCloseAll();
return 1;
}