631 lines
30 KiB
Markdown
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
|