WinComm/drv/README.md
2026-10-12 16:22:16 -05:00

631 lines
30 KiB
Markdown

# 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. The rewrite
> compiles and links under Open Watcom with the Win16 headers but has not yet
> been built with MSVC 1.52 or run on hardware. See
> [Verification checklist](#verification-checklist).
## Contents
- [Files](#files)
- [Building](#building)
- [Installing](#installing)
- [Features](#features)
- [SYSTEM.INI settings](#systemini-settings)
- [DCB support](#dcb-support)
- [Escape functions](#escape-functions)
- [Events, status, and errors](#events-status-and-errors)
- [Notifications (WM_COMMNOTIFY)](#notifications-wm_commnotify)
- [Parallel ports](#parallel-ports)
- [Exported entry points](#exported-entry-points)
- [Architecture](#architecture)
- [Differences from the stock driver](#differences-from-the-stock-driver)
- [Debug logging](#debug-logging)
- [Verification checklist](#verification-checklist)
- [Future work](#future-work)
- [References](#references)
## 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 |
| `ring0.md` | Notes for moving interrupt handling to ring 0, if it is ever needed |
## 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](#notifications-wm_commnotify)), 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`.
A compile check without MSVC is possible with Open Watcom 2.0 (Win16 target,
small model, SS != DS):
```
wcc -q -bt=windows -ms -zu -zw -bd -zp2 -2 -w4 commdrv.c
```
Expect W200 "referenced but never assigned" warnings for variables set
inside `_asm` blocks; Watcom does not see those assignments.
## Installing
1. Copy `KPCOMM.DRV` to `\WINDOWS\SYSTEM`.
2. In `SYSTEM.INI` `[boot]`, set `comm.drv=kpcomm.drv`.
3. Optionally add [SYSTEM.INI settings](#systemini-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](#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](#escape-functions)). |
Booleans accept `1`-`9`, `true`, `yes`, `on` and `0`, `false`, `no`, `off`.
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](#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](#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
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](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.html`
on 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