// 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 // ----------------------------------------------------------------------- // SYSTEM.INI [386Enh] keys, named COMn // ----------------------------------------------------------------------- #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, 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" 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 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 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 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 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; }