WinComm/drv
2026-10-14 21:18:59 -05:00
..
commdrv.c License headers added. 2026-10-13 14:05:57 -05:00
commdrv.def License headers added. 2026-10-13 14:05:57 -05:00
commdrv.h License headers added. 2026-10-13 14:05:57 -05:00
isr.c License headers added. 2026-10-13 14:05:57 -05:00
kpcomm.lnk License headers added. 2026-10-13 14:05:57 -05:00
LICENSE.md License added. 2026-10-13 13:58:31 -05:00
lpt.c License headers added. 2026-10-13 14:05:57 -05:00
makefile License headers added. 2026-10-13 14:05:57 -05:00
README.md README updated. 2026-10-14 21:18:59 -05:00
ring0.md Basically a complete rewrite. 2026-10-12 16:22:16 -05:00
watcom.mk License headers added. 2026-10-13 14:05:57 -05:00

KPCOMM.DRV -- High-Speed COMM.DRV Replacement for Windows 3.1

KPCOMM.DRV is a drop-in replacement for the Windows 3.1 communications driver (COMM.DRV). It exists to make 57600 and 115200 baud reliable on 16550A UARTs, and adds a few extensions the stock driver lacks. Every Windows application that uses the comm API (OpenComm, ReadComm, WriteComm, ...) goes through it once installed.

The exported interface follows the communication-driver chapter of the Windows 3.1 Device Driver Adaptation Guide (DDAG). Where the DDAG is silent, behavior follows the Windows 3.1 SDK, Wine's Win16 comm code, or the stock driver as far as it is known; those cases are called out below.

Status: the driver was rewritten to the DDAG on 2026-10-12. Built with Open Watcom (watcom.mk), it runs: JoeyLib's Win16 port installs it as comm.drv on Windows for Workgroups 3.11 under DOSBox Staging, where COM1 opens and echoes, and 16 KB go out and come back in order at the full line rate at 57600 and at 115200 (KPCBR_115200). It has not yet been built with MSVC 1.52 or run on hardware. See Verification checklist.

Contents

Files

File Contents
commdrv.h Constants (UART, PIC, DPMI, BIOS), the ComDEB/port/LPT/qdb structures, prototypes shared between the C files
commdrv.c Exported entry points, DCB handling, ring buffers, SYSTEM.INI and BIOS configuration, LibMain/WEP
isr.c Interrupt entry points, per-IRQ hook table, UART interrupt dispatch, transmit/flow-control/notification helpers
lpt.c LPT1-LPT3 support (polled output through BIOS INT 17h)
commdrv.def Module definition: module name COMM, export ordinals, FIXED segments
makefile NMAKE build for MSVC 1.52
watcom.mk GNU make build with Open Watcom v2 (on Linux)
kpcomm.lnk wlink directives for the Open Watcom build: what commdrv.def gives MSVC's linker
ring0.md Notes for moving interrupt handling to ring 0, if it is ever needed
LICENSE.md The MIT License

Building

Requires MSVC 1.52 (cl, link, nmake) on the path.

cd drv
nmake          builds kpcomm.drv
nmake clean    removes objects, map, and driver

Compiler flags (see makefile): small model with SS != DS (-ASw), 286 instructions (-G2), Windows prolog without stack probes (-Gsw), safe optimizations (-Ow), 2-byte structure packing (-Zp2). Port I/O, CLI/STI, and far memory helpers are forced inline with #pragma intrinsic in commdrv.h. The driver links sdllcew and libw with no default libraries.

Two rules matter for anyone changing the code:

  • No USER imports. COMM.DRV is loaded before USER.EXE, so it may import only from KERNEL. PostMessage is looked up at run time (see Notifications), and timing uses the BIOS tick count instead of GetTickCount (a USER function in Win16). lstrcmpi is also USER-only, which is why SYSTEM.INI Booleans are parsed by hand.
  • No near pointers to locals. With SS != DS a near pointer is DS-relative, so any out-parameter that can point at a stack variable is declared FAR.

With Open Watcom

watcom.mk builds the same driver with Open Watcom v2's 16-bit Windows target, with GNU make on Linux. It uses the Open Watcom JoeyLib's installer sets up (../../joeylib/toolchains/opt/openWatcom) unless WATCOM names another; it needs the 16-bit Windows libraries (lib286/win).

cd drv
make -f watcom.mk                   builds bin/kpcomm.drv
make -f watcom.mk clean             removes obj/ and bin/
make -f watcom.mk OBJ=DIR BIN=DIR   builds somewhere else

The flags match the MSVC build's (see watcom.mk), with no stack probes (-s: Watcom adds them by default). kpcomm.lnk carries commdrv.def's module name, description, fixed segments, and export ordinals; the sources' _export exports the entry points too, which wlink's warning W1121 reports and the link file turns off. The W200 "referenced but never assigned" warnings for variables set inside _asm blocks are turned off as well (-wcd=200): Watcom does not see those assignments, and the code it generates for them is right (they are memory operands). JoeyLib's installer builds the driver this way (its kpcomm component), into its own build tree.

Installing

  1. Copy KPCOMM.DRV to \WINDOWS\SYSTEM.
  2. In SYSTEM.INI [boot], set comm.drv=kpcomm.drv.
  3. Optionally add SYSTEM.INI settings to [386Enh].
  4. Restart Windows.

To uninstall, set comm.drv=comm.drv again and restart.

The module name inside the DLL stays COMM (see commdrv.def) because USER.EXE imports the driver by that name; only the file is called KPCOMM.DRV, so it never overwrites the stock COMM.DRV.

Features

  • 16550A FIFOs, both directions. Detects a working 16550A FIFO at open (the original 16550 with its broken FIFO is treated as a 16450). The RX trigger level is configurable (default 4 bytes). The TX FIFO is filled with up to 16 bytes per transmitter-empty interrupt; the stock driver uses the RX FIFO only and sends one byte per interrupt.
  • Any common IRQ. IRQ 3, 4, 5, 7, 9, 10, 11, 12, and 15 (an ISA card jumpered to IRQ 2 is IRQ 9). Several ports can share one IRQ. Interrupts none of our ports claim are passed to the previous handler when another device owned the line first.
  • Flow control, every DCB flavor. XON/XOFF in each direction separately (fOutX, fInX), RTS/CTS, DTR/DSR, and CTS/DSR/RLSD through the DCB timeouts. Throttling is recorded and always undone, including on flush, queue resize, and settings change.
  • Modem-line wait timeouts. The DCB's CtsTimeout, DsrTimeout, and RlsTimeout, with CE_CTSTO, CE_DSRTO, and CE_RLSDTO errors.
  • Receive options. Parity-error replacement, NUL stripping, event character, EOF detection in non-binary mode.
  • Events and WM_COMMNOTIFY notifications.
  • Close without hanging up. Optionally keeps DTR asserted through CloseComm and even Windows exit, so a modem stays online for the next program.
  • LPT1-LPT3, as the stock COMM.DRV provides (used by direct-to-port printing).
  • Configurable resources. Base address and IRQ from the BIOS data area or SYSTEM.INI; minimum queue size; FIFO on/off.
  • Low interrupt latency in the driver itself. Reads and writes copy data with interrupts enabled; only counter updates are guarded. Port I/O is inline. Ring buffers are page-locked.

SYSTEM.INI settings

All settings go in [386Enh]; n is the port number (1-4). Keys are case-insensitive.

Key Values Default Meaning
COMnFIFO Boolean on Use the 16550A FIFOs. 0/false/no/off disables them.
COMnRxTRIGGER 1, 4, 8, 14 4 RX FIFO trigger level in bytes. Other values fall back to 4. See RX trigger level.
COMnBuffer bytes 64 Minimum queue size. Each queue is the larger of this and what the application asked for in OpenComm.
COMnBase hex 3F8/2F8/3E8/2E8 UART base address (3E8, 0x3E8, or 3E8h). Used only when the BIOS data area has no address for the port.
COMnIRQ 2-15 4/3/4/3 IRQ line. 2 means 9. Unsupported values make OpenComm fail with IE_HARDWARE.
COMnKeepDTR Boolean off Leave DTR asserted when the port is closed (see Escape functions).

Booleans accept 1-9, true, yes, and on for true, and 0, false, no, and off for false.

Notes:

  • Base address precedence. The BIOS data area (segment 0040h) wins and SYSTEM.INI only fills a missing entry, as the DDAG specifies. On a machine whose BIOS lists a single port at 2F8 as COM1, COM1 is 2F8.
  • COMnBuffer is shared with VCD. Windows' virtual COM device also reads COMnBuffer, as the buffer size for DOS applications (default 128). KPCOMM treats it only as a floor, so a value set for DOS applications can only make Windows queues larger.
  • COMnFIFO. The DDAG says only 1 enables the FIFO; KPCOMM accepts any SYSTEM.INI Boolean, because COMnFIFO=true is common.

RX trigger level

The trigger level trades interrupt rate against how long the handler may be late before the 16-byte FIFO overruns. Figures at 115200 baud, one byte per 87 us:

Trigger Max interrupts/s Slack before overrun Trailing bytes delivered
1 ~11,500 ~1.3 ms (15 bytes) at once
4 (default) ~2,900 ~1.0 ms (12 bytes) within ~0.35 ms
8 ~1,400 ~0.7 ms (8 bytes) within ~0.35 ms
14 ~800 ~0.17 ms (2 bytes) within ~0.35 ms (the stock driver's level)

The interrupt rate falls on its own under load, because a late interrupt drains several bytes.

When fewer bytes than the trigger are waiting, the 16550A's character-timeout interrupt hands them over after 4 character times (0.35 ms at 115200, 17 ms at 2400), so a higher trigger costs no visible latency. That depends on the UART actually raising the timeout interrupt. If typed characters echo late, or a burst's last few bytes only appear when more data arrives, set COMnRxTRIGGER=1 for that port. The setting has no effect on 8250/16450 UARTs, or when the FIFO is disabled.

DCB support

The DCB is validated before anything is programmed. A bad value fails OpenComm/SetCommState with the listed code and leaves the port unchanged.

Field Handling
Id 0-3 = COM1-COM4, 0x80-0x82 = LPT1-LPT3
BaudRate Raw rates 2-0xFEFF, or a CBR index (high byte 0xFF): CBR_110 through CBR_56000, plus the KPCOMM extension KPCBR_115200 (0xFF24). CBR_128000/CBR_256000 are beyond a 1.8432 MHz UART. Invalid: IE_BAUDRATE.
ByteSize 5-8, else IE_BYTESIZE
Parity NOPARITY, ODDPARITY, EVENPARITY, MARKPARITY, SPACEPARITY, else IE_DEFAULT
StopBits ONESTOPBIT; ONE5STOPBITS and TWOSTOPBITS both set the UART's extra-stop bit (1.5 stop bits with 5 data bits, 2 otherwise); else IE_DEFAULT
RlsTimeout, CtsTimeout, DsrTimeout 0 = ignore the line. 0xFFFF = flow control (output held while the line is low). Otherwise milliseconds to wait for the line before output is accepted; see Line timeouts.
fBinary When 0, receiving EofChar sets CSTF_EOF. The byte is still stored.
fRtsDisable, fDtrDisable Line off at open, and whenever the flag changes in SetCommState. A SetCommState that leaves the flag alone does not undo EscapeCommFunction.
fParity, fPeChar, PeChar With both flags set, a byte received with a parity error is replaced by PeChar. CE_RXPARITY is reported either way.
fOutxCtsFlow, fOutxDsrFlow Output held while CTS / DSR is low
fOutX, XonChar, XoffChar Received XOFF/XON stop and restart output; the characters are not stored
fInX, XoffLim, XonLim Send XOFF when no more than XoffLim bytes are free, XON when no more than XonLim remain. Limits that do not fit the queue are clamped.
fRtsflow, fDtrflow Drop RTS / DTR at the same thresholds to pause the sender
fNull Received NUL bytes are discarded
fChEvt, EvtChar Receiving EvtChar raises EV_RXFLAG
TxDelay Ignored (unused in Windows 3.1)

115200 baud

The 16-bit BaudRate cannot hold 115200 as a raw value. Two routes work:

  • Any raw value from 57601 to 65279. The divisor (115200 / rate) is deliberately truncated, so these give divisor 1. The earlier driver code recorded that BuildCommDCB stores 0xE101 (57601) for "115200", which this relies on; confirm it on real Windows.
  • KPCBR_115200 (0xFF24), a KPCOMM-only index. Other drivers reject it.

Line timeouts

CtsTimeout, DsrTimeout, and RlsTimeout follow the DDAG: each is the time to wait for the line to go high before transmitting, and 0xFFFF is how a DCB asks for CTS, DSR, or RLSD flow control.

  • 0 (IGNORE): the line is not checked.
  • 0xFFFF (INFINITE): the interrupt handler holds output while the line is low, with no error. RLSD/DCD flow control exists only this way.
  • Anything else: WriteComm (CommWriteString), sndcom, TransmitCommChar (ctx), and the drain at CloseComm first wait up to that many milliseconds for every guarded line to be high. If one stays low, nothing is queued and GetCommError reports CE_CTSTO, CE_DSRTO, or CE_RLSDTO. WriteComm returns 0; TransmitCommChar returns 0x4000.

The wait blocks the calling application (55 ms resolution, from the BIOS tick). As far as is known the stock driver's MSRWait behaved the same way. USER's default values for these fields are unknown; TKPComm sets all three to 0.

Escape functions

EscapeCommFunction codes handled by cextfcn:

Code Value COM ports LPT ports
SETXOFF 1 Hold output as if XOFF were received --
SETXON 2 Release that hold --
SETRTS / CLRRTS 3 / 4 RTS on/off (receive flow control may still hold it low) --
SETDTR / CLRDTR 5 / 6 DTR on/off (likewise) --
RESETDEV 7 No-op Pulse the printer's reset line (INT 17h AH=1)
GETMAXLPT 8 Highest LPT cid the BIOS reports (0x80 = LPT1) same
GETMAXCOM 9 3 (COM4), as the DDAG specifies same
GETBASEIRQ 10 Base address in the low word, IRQ in the high word; high word 0xFFFF if the port does not exist. Works on unopened ports. same (IRQ 7 for LPT1, 5 otherwise)
KPESC_KEEP_DTR_ON_CLOSE 0x4B50 KPCOMM extension: closing leaves DTR asserted --
KPESC_DROP_DTR_ON_CLOSE 0x4B51 KPCOMM extension: closing drops DTR (the default) --

Returns are 0 on success, negative for an unknown code or port, or the value requested. Unlike Win32, a 16-bit EscapeCommFunction returns 0 for success. The stock driver rejects the KPCOMM codes, so software can safely send them without knowing which driver is installed.

Keeping a connection up across close

By default closing a port drops DTR and RTS, which hangs up a modem set to AT&D2. To keep the connection, either set COMnKeepDTR=1, which suits applications you cannot change, or send KPESC_KEEP_DTR_ON_CLOSE before CloseComm. TKPComm does the latter when HangupOnClose is False.

When DTR is kept, close still turns off RTS and OUT2, the bit that connects the UART to its IRQ line. With RTS/CTS flow control the modem then holds incoming data until the next program raises RTS. The flag also applies when Windows exits with the port open (WEP), so the line can outlive Windows. It is not yet confirmed that VCD leaves the UART alone when enhanced mode shuts down. If USER turns out not to pass private escape codes through to the driver, use the SYSTEM.INI key. AT&D0 (modem ignores DTR) is the fallback.

Events, status, and errors

Events (SetCommEventMask / GetCommEventMask). Only events enabled in the mask are recorded in the event word.

Event Raised when
EV_RXCHAR A byte is stored in the receive queue
EV_RXFLAG EvtChar is received (fChEvt)
EV_TXEMPTY The transmit queue and FIFO have drained
EV_CTS, EV_DSR, EV_RLSD The line changed state
EV_RING Ring indicator trailing edge
EV_BREAK Break received
EV_ERR Overrun, parity, or framing error

Not generated: EV_PERR and the Windows 3.1 line-state bits EV_CTSS, EV_DSRS, EV_RLSDS, EV_RINGTE. Third-party code that wants line state can read the MSR shadow at offset 35 of the block returned by SetCommEventMask (KB Q101417).

COMSTAT status (GetCommError): CSTF_CTSHOLD, CSTF_DSRHOLD, CSTF_RLSDHOLD, CSTF_XOFFHOLD (output held for that reason), CSTF_XOFFSENT (we sent XOFF), CSTF_EOF, CSTF_TXIM (a TransmitCommChar character is pending).

Errors accumulate until GetCommError reads and clears them: CE_RXOVER (receive queue full), CE_OVERRUN (UART overrun), CE_RXPARITY, CE_FRAME, CE_BREAK, CE_TXFULL (WriteComm could not queue everything), CE_CTSTO/CE_DSRTO/CE_RLSDTO (line timeouts). For LPT ports: CE_PTO, CE_IOE, CE_DNS, CE_OOP. An invalid cid returns CE_MODE.

ReadComm does not return a negative count when errors are pending; whether USER or the driver should do that is undocumented. Call GetCommError.

Notifications (WM_COMMNOTIFY)

EnableCommNotification works as documented; KPTEST polls and does not use it. PostMessage comes from USER at run time (GetProcAddress(GetModuleHandle("USER"), "PostMessage")), since COMM.DRV cannot import USER statically. It is one of the functions Windows 3.1 allows at interrupt time. wParam is the cid; the low word of lParam holds the CN_* bits.

Notification Posted when Re-armed when
CN_RECEIVE Data is waiting The application reads or flushes input
CN_TRANSMIT Fewer than cbOutQueue bytes remain to send The queue is refilled to that level
CN_EVENT An enabled event is pending The application calls GetCommEventMask

A trigger of -1 disables that notification; a NULL window disables all.

The DDAG allows CN_RECEIVE with fewer than cbWriteNotify bytes once "an internal timeout" expires. KPCOMM has no timer, so that timeout is zero: CN_RECEIVE is posted as soon as any data is waiting. Applications must already cope with short reads, because the stock driver sends them after its timeout. Re-arming happens when the application acknowledges, so data that arrives while it is reading is announced again. A post that fails because the application's queue is full is retried on the next interrupt.

Parallel ports

LPT1-LPT3 (cid 0x80-0x82) work as with the stock driver, which Windows uses for direct-to-port printing. Output goes through BIOS INT 17h one byte at a time; Windows reflects it to the real-mode BIOS in standard and enhanced mode. A write stops at the first byte the printer rejects and records the reason for GetCommError. OpenComm fails with IE_HARDWARE if the BIOS lists no such port. The BIOS waits its own timeout when a printer is offline, which stalls the caller -- presumably as the stock driver did. Input, events, line settings, and break are accepted but do nothing.

Exported entry points

The callee pops its parameters (Pascal convention), so the RETF size must match the caller exactly or USER.EXE faults. Signatures follow the DDAG. SETCOM and GETDCB were also confirmed by disassembling CyberCom.

Ord Export RETF Parameters Returns
1 INICOM 4 DCB FAR * (cid in Id) 0, or IE_*
2 SETCOM 4 DCB FAR * 0, or IE_*
3 SETQUE 6 int cid, qdb FAR * 0, IE_BADID, IE_NOPEN, IE_MEMORY
4 RECCOM 2 int cid The byte, -2 if none, -1 on error
5 SNDCOM 4 int cid, int ch 0, or nonzero on failure
6 CTX 4 int cid, int ch 0, 0x4000 busy, 0x8000 bad cid
7 TRMCOM 2 int cid 0, -2 output not sent, 0x8000 bad cid
8 STACOM 6 int cid, COMSTAT FAR * Error word
9 CEXTFCN 4 int cid, int fcn LONG (see escapes)
10 CFLUSH 4 int cid, int q (0 = transmit, 1 = receive) 0 or -1
11 CEVT 4 int cid, int mask Pointer to the event word
12 CEVTGET 4 int cid, int mask Events in mask, which are cleared
13 CSETBRK 2 int cid 0 or -1
14 CCLRBRK 2 int cid 0 or -1
15 GETDCB 2 int cid DCB FAR * in DX:AX
16 WEP 2 int 1
17 SUSPENDOPENCOMMPORTS 0 -- --
18 REACTIVATEOPENCOMMPORTS 0 -- --
19 COMMWRITESTRING 8 int cid, LPSTR, int len Bytes queued
20 READCOMMSTRING 8 int cid, LPSTR, int len Bytes read
100 ENABLENOTIFICATION 8 int cid, HWND, int rxTrigger, int txTrigger BOOL
101 COMMNOTIFYWNDPROC 0 -- 0 (stub)

Notes:

  • USER treats the presence of COMMWRITESTRING as the sign of a 3.1-style driver and then uses the block calls. RECCOM/SNDCOM are the single-byte 3.0 interface.
  • READCOMMSTRING (20) is not in the DDAG; it matches the stock driver's export.
  • SETQUE receives a DDAG queue definition block (qdb): receive buffer and size, transmit buffer and size. KPCOMM uses only the sizes and keeps its own page-locked buffers.
  • 101 is undocumented; CyberCom implements it as a plain RETF.
  • USER calls INICOM, then SETQUE, for every OpenComm.

Suspend/Reactivate (17/18) are called by WINOLDAP around a switch to a DOS application. Suspend turns off every open port's interrupt enables and passes the IRQs to the previous handlers; DTR and RTS are untouched, so the modem stays online. Reactivate reprograms every UART from the saved state (divisor, line control, FIFOs, modem lines), then takes the IRQs back and unmasks them. Queued data is kept.

Architecture

State

Each COM port has a PortStateT in the fixed data segment. Its first member is a 40-byte, stock-compatible ComDebT, whose address SetCommEventMask returns. The ComDEB fields are the driver's live state, not copies, so third-party readers always see current values:

Offset Field Meaning
+0 evtWord Event word
+2 evtMask Enabled events
+6 / +8 / +10 / +12 rxSize, rxCount, rxHead, rxTail Receive ring
+16 / +18 / +20 / +22 txSize, txCount, txHead, txTail Transmit ring
+24 baseAddr UART base address
+26 baudRate Mirror of dcb.BaudRate, written only by applyDcb
+28 / +29 / +30 lcrShadow, mcrShadow, ierShadow Last values written to LCR (including break), MCR, IER
+34 fifoTrigger FCR trigger bits
+35 msrShadow Last MSR value (KB Q101417; Wine uses the same offset)

Only offsets 0 and 35 are documented as stock-compatible; the rest follow the layout chosen when the ComDEB was introduced.

The port's DCB is the single source for line and flow settings; the interrupt handler reads its fields directly. Other per-port state:

Field Meaning
txHold Why output is paused: remote XOFF, CTS/DSR/RLSD low
rxThrottle What was done to pause the sender: XOFF sent, RTS or DTR dropped
flowChar XON/XOFF to send next (NO_CHAR if none)
txImmediate TransmitCommChar character (NO_CHAR if none)
dtrOn, rtsOn What the application wants for DTR and RTS

Interrupt path

  • Hooking. Each supported IRQ has a small _interrupt entry stub (isrIrqNN). The stub loads the driver's DS, clears the direction flag, and calls serviceIrq. Vectors are installed with DPMI INT 31h 0205h on the protected-mode vector (08h + IRQ, or 70h + IRQ - 8). A per-IRQ table keeps the previous vector, a reference count of open ports, and whether the IRQ was masked before the first hook. The line is held masked while that state is set up.
  • Dispatch. serviceIrq runs servicePort on every open port on the IRQ. servicePort reads IIR until nothing is pending (at most 64 times). When several UARTs share an edge-triggered line, passes repeat until a full pass finds nothing pending (at most 32). Otherwise an edge raised mid-service would be lost and the IRQ would go dead.
  • Ending the interrupt:
    • Claimed by one of our ports: send EOI (slave PIC first for IRQ 8-15).
    • Not claimed, and another device may own the line (it was unmasked before KPCOMM hooked it, or the ports are suspended for a DOS application): chain to the previous handler, which sends its own EOI.
    • Not claimed, and the line is ours alone: a late edge from our own UART, so send EOI. The exception is a spurious IRQ 7 or 15 that the PIC never put in service, checked with an OCW3 in-service read. It gets no EOI (IRQ 15 still sends one to the master for the cascade).
  • Closing. The last port on an IRQ restores the original mask bit and vector; other ports on the IRQ keep running.

Transmit

THR is written in exactly one place: handleTx, on a transmitter-empty interrupt, when the holding register and FIFO are known to be empty. The order is our own XON/XOFF first (sent even while the remote has us stopped, but not while a CTS/DSR/RLSD hold is on), then the TransmitCommChar character, then up to 16 queued bytes. If nothing may be sent, the transmitter-empty interrupt is turned off.

Everything else calls startTx, which only turns that interrupt back on. On a 16450/16550, switching it from off to on while the transmitter is idle raises an interrupt at once, so there is never a direct write that could overflow the FIFO.

Receive

handleRx drains the UART (at most 64 bytes per call; an all-ones LSR stops it, since a vanished UART reads 0xFF). Per byte it applies, in order: parity replacement, XON/XOFF recognition, NUL stripping, the event character, EOF detection, and storing. Afterwards updateRxFlow compares the fill level against the XOFF/XON limits.

Ring buffers

Each port has one producer and one consumer per ring. The application moves rxTail and txHead; the interrupt handler moves rxHead and txTail. Because of that, readRx and writeTx copy with interrupts enabled. They disable interrupts only to update rxCount/txCount and to run the flow-control and notification bookkeeping. Index wrap is computed so it cannot overflow 16 bits, even for queues near 64 KB.

Buffers come from GlobalAlloc(GMEM_FIXED | GMEM_SHARE) plus GlobalPageLock:

  • Stable address: FIXED memory never moves.
  • No page faults: page-locked memory is never paged out, so the interrupt handler cannot fault on it in enhanced mode.
  • Driver-owned: GMEM_SHARE makes the driver own the memory, not the calling task, so an application that dies without CloseComm cannot free a buffer the interrupt handler is still writing.

Allocation happens before interrupts are disabled, because the global heap may need DPMI services.

Open and close

  • OpenComm (inicom, then setque):

    1. Validate the DCB.
    2. Find the base address and IRQ; check that a UART answers (an LCR read-back test).
    3. Detect the FIFO; allocate default-size queues.
    4. Silence the UART and clear anything pending, then hook the IRQ.
    5. Enable the FIFOs and apply the DCB.
    6. Take a fresh MSR snapshot in the same guarded step that turns on the receive, line-status, and modem-status interrupts.

    setque then resizes the queues (only if the size changed) and empties them.

  • CloseComm (trmcom):

    1. Wait for queued output to drain. It gives up after about 5 s without progress, or at once if a timed modem line stays low.
    2. Disable the port, reset the FIFOs, and drop RTS, OUT2, and (unless kept) DTR.
    3. Clear the UART's pending status, unhook the IRQ, and free the buffers.

    At Windows exit, WEP closes any open port the same way, without the drain.

Timing

There is no timer. Waits (the drain at close, line timeouts) count changes of the BIOS tick at 0040:006C, read through a DPMI selector for segment 0040h. Counting changes, rather than comparing values, makes them immune to the midnight rollover.

Differences from the stock driver

Area Stock COMM.DRV (3.1) KPCOMM.DRV
FIFO use RX only, trigger 14 RX and TX, trigger configurable (default 4), 16-byte TX bursts
IRQs Ring-0 handler registered with VCD in enhanced mode (KB Q80901) Ring-3 DPMI handler (see Future work)
Queues Buffers allocated by USER Own page-locked buffers, sizes from OpenComm with a COMnBuffer floor
Close Drops DTR and RTS Same, unless asked to keep DTR
CN_RECEIVE below trigger After an internal timeout Immediately (zero timeout)
Baud CBR indexes up to 256000, raw values 1.8432 MHz limits: up to 115200 via raw 57601-65279 or KPCBR_115200
Extensions -- KPESC_*_DTR_ON_CLOSE, COMnKeepDTR, COMnRxTRIGGER, Boolean COMnFIFO

Debug logging

Set DBG_ENABLED to 1 in commdrv.h and rebuild. The driver then logs via OutputDebugString (view with DBWIN or a debugging terminal):

  • inicom: the port id and the three DCB timeouts. This shows what USER's default DCB holds.
  • setque: the queue sizes received. Expect the application's OpenComm sizes, e.g. 4096 for KPTEST.
  • setcom: the baud value. Shows what BuildCommDCB produced for "115200".
  • trmcom, suspend, reactivate, LibMain.

Logging never happens inside the interrupt handler or with interrupts disabled.

Verification checklist

Confirmed in emulation (2026-10-12): an Open Watcom build on Windows for Workgroups 3.11 under DOSBox Staging, through JoeyLib's Win16 port and its gates. COM1 opens and closes (item 4, COM1 only), and 16 KB go out and come back in order at the full line rate at 57600 and at 115200 through KPCBR_115200 (items 3 and 6, without flow control and without other load); Windows' own COMM.DRV refuses KPCBR_115200, as it should.

Not yet confirmed on hardware with an MSVC 1.52 build:

  1. Build. It compiles and links cleanly under MSVC 1.52, including #pragma intrinsic, static _interrupt functions, and MAKELP.
  2. setque. The debug log shows sane queue sizes; this confirms the qdb signature.
  3. 115200. A settings string of "115200" reaches 115200 baud.
  4. Open and close. COM1 through COM4 open (inicom now returns 0, not the cid).
  5. Trigger 4. At the default trigger of 4, single typed characters echo immediately, which shows the 16550A character-timeout interrupt works. If they lag, use COMnRxTRIGGER=1 there and note which UART or emulator it was.
  6. Sustained transfer. A sustained 115200 transfer with RTS/CTS and with XON/XOFF shows no overruns or stalls, including while copying files, reading a floppy, and running a DOS box.
  7. Shared IRQ. COM1 + COM3 on one IRQ; closing one leaves the other working. A serial mouse on COM1 keeps working while COM3 is open.
  8. Non-standard IRQ. COMnIRQ=5 (or another non-standard IRQ) works.
  9. Printing. Direct-to-port printing on LPT1 works.
  10. Keep DTR. HangupOnClose=False keeps the modem online across close and across Windows exit. If not, try COMnKeepDTR=1.
  11. Notifications. A WM_COMMNOTIFY-driven application (e.g. a VB MSComm program) receives and sends.
  12. DOS switch. Suspend/Reactivate around a full-screen DOS application in standard mode.

Future work

  • Ring-0 interrupt handling. Not needed unless measurements show overruns under realistic load; see ring0.md for the analysis, measurement plan, and implementation options.
  • Latency instrumentation. A debug counter of the most bytes drained per receive interrupt, plus an overrun count, would show how much FIFO headroom is left (step 1 of the plan in ring0.md).
  • A timer for CN_RECEIVE. It would allow a real "fewer bytes after timeout" CN_RECEIVE instead of immediate posting.

References

License

KPCOMM is distributed under the MIT License (LICENSE.md): it may be used, changed, and shipped, alone or with a program, as source or as the built driver, as long as the copyright and permission notice go with it. The license covers this folder; the rest of the repository is not under it. Each source file here opens with a short header that says so (SPDX-License-Identifier: MIT).

Built with Open Watcom (watcom.mk), the driver also carries Open Watcom code (its DLL start-up and C library helpers), and 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 that code is available and where. The built kpcomm.drv carries the notice (never shown); a program that ships it puts this line in its documentation:

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

The MSVC build carries none of Open Watcom's code.