| .. | ||
| commdrv.c | ||
| commdrv.def | ||
| commdrv.h | ||
| isr.c | ||
| kpcomm.lnk | ||
| LICENSE.md | ||
| lpt.c | ||
| makefile | ||
| README.md | ||
| ring0.md | ||
| watcom.mk | ||
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 ascomm.drvon 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
- Building
- Installing
- Features
- SYSTEM.INI settings
- DCB support
- Escape functions
- Events, status, and errors
- Notifications (WM_COMMNOTIFY)
- Parallel ports
- Exported entry points
- Architecture
- Differences from the stock driver
- Debug logging
- Verification checklist
- Future work
- References
- License
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.
PostMessageis looked up at run time (see Notifications), and timing uses the BIOS tick count instead ofGetTickCount(a USER function in Win16).lstrcmpiis 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
- Copy
KPCOMM.DRVto\WINDOWS\SYSTEM. - In
SYSTEM.INI[boot], setcomm.drv=kpcomm.drv. - Optionally add SYSTEM.INI settings to
[386Enh]. - 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, andRlsTimeout, withCE_CTSTO,CE_DSRTO, andCE_RLSDTOerrors. - 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.
COMnBufferis shared with VCD. Windows' virtual COM device also readsCOMnBuffer, 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 only1enables the FIFO; KPCOMM accepts any SYSTEM.INI Boolean, becauseCOMnFIFO=trueis 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 reportsCE_CTSTO,CE_DSRTO, orCE_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
COMMWRITESTRINGas the sign of a 3.1-style driver and then uses the block calls.RECCOM/SNDCOMare the single-byte 3.0 interface. READCOMMSTRING(20) is not in the DDAG; it matches the stock driver's export.SETQUEreceives 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.101is undocumented; CyberCom implements it as a plain RETF.- USER calls
INICOM, thenSETQUE, 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
_interruptentry stub (isrIrqNN). The stub loads the driver's DS, clears the direction flag, and callsserviceIrq. 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.
serviceIrqrunsservicePorton every open port on the IRQ.servicePortreads 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, thensetque):- Validate the DCB.
- Find the base address and IRQ; check that a UART answers (an LCR read-back test).
- Detect the FIFO; allocate default-size queues.
- Silence the UART and clear anything pending, then hook the IRQ.
- Enable the FIFOs and apply the DCB.
- Take a fresh MSR snapshot in the same guarded step that turns on the receive, line-status, and modem-status interrupts.
setquethen resizes the queues (only if the size changed) and empties them. -
CloseComm (
trmcom):- 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.
- Disable the port, reset the FIFOs, and drop RTS, OUT2, and (unless kept) DTR.
- Clear the UART's pending status, unhook the IRQ, and free the buffers.
At Windows exit,
WEPcloses 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:
- Build. It compiles and links cleanly under MSVC 1.52, including
#pragma intrinsic,static_interruptfunctions, andMAKELP. - setque. The debug log shows sane queue sizes; this confirms the qdb signature.
- 115200. A settings string of "115200" reaches 115200 baud.
- Open and close. COM1 through COM4 open (
inicomnow returns 0, not the cid). - 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=1there and note which UART or emulator it was. - 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.
- Shared IRQ. COM1 + COM3 on one IRQ; closing one leaves the other working. A serial mouse on COM1 keeps working while COM3 is open.
- Non-standard IRQ.
COMnIRQ=5(or another non-standard IRQ) works. - Printing. Direct-to-port printing on LPT1 works.
- Keep DTR.
HangupOnClose=Falsekeeps the modem online across close and across Windows exit. If not, tryCOMnKeepDTR=1. - Notifications. A WM_COMMNOTIFY-driven application (e.g. a VB MSComm program) receives and sends.
- 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
- Windows 3.1 DDAG, communication drivers:
https://dos-help.soulsphere.org/ddag31qh.hlp/Communication-Driver_Functions.html
(see also
inicom.html,setque.html,qdb.html,cextfcn.html,trmcom.html,DCB.html,commbaud.html,commirq.html,commfifo.htmlon the same site) - Wine's Win16 comm implementation (
dlls/user.exe16/comm.c) and Win16 definitions (include/wine/winuser16.h) - KB Q80901, Windows 3.1 application compatibility (COMM.DRV ring-0 handler,
COMMDRV30): https://jeffpar.github.io/kbarchive/kb/080/Q80901/ - KB Q83449, Windows 3.1 serial communications Q&A
- KB Q101417, the MSR shadow at offset 35 of the ComDEB
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.