Basically a complete rewrite.

This commit is contained in:
Scott Duensing 2026-10-12 16:22:16 -05:00
parent 0e8bb9f989
commit 9265a76b65
9 changed files with 3635 additions and 1988 deletions

View file

@ -37,6 +37,7 @@ type
{ Modem control lines }
FDTREnable: Boolean; { DTR line state (True=asserted) }
FRTSEnable: Boolean; { RTS line state (True=asserted) }
FHangupOnClose: Boolean; { Drop DTR on close (False keeps a modem online) }
{ DCB options }
FNullDiscard: Boolean; { Strip null bytes from received data }
@ -53,6 +54,7 @@ type
function GetInput: string;
function GetOutBufferCount: Integer;
procedure OpenPort;
procedure PrepareDCB(var DCB: TDCB);
procedure SetBreak(Value: Boolean);
procedure SetCommPort(Value: Integer);
procedure SetDTREnable(Value: Boolean);
@ -85,6 +87,7 @@ type
property InputMode: TInputMode read FInputMode write FInputMode default imText;
property DTREnable: Boolean read FDTREnable write SetDTREnable default True;
property RTSEnable: Boolean read FRTSEnable write SetRTSEnable default True;
property HangupOnClose: Boolean read FHangupOnClose write FHangupOnClose default True;
property NullDiscard: Boolean read FNullDiscard write SetNullDiscard default False;
property EOFEnable: Boolean read FEOFEnable write FEOFEnable default False;
property ParityReplace: string read FParityReplace write SetParityReplace;
@ -95,22 +98,26 @@ procedure Register;
implementation
const
{ DCB Flags field bit masks. The Windows 3.1 DCB packs thirteen 1-bit }
{ fields into a single UINT (Word) at offset 12 of the structure. }
{ Delphi 1.0 maps this UINT to TDCB.Flags. }
{ DCB Flags field bit masks. The Windows 3.1 DCB packs its 1-bit }
{ fields into a single UINT (Word) at offset 12 of the structure, with }
{ a 2-bit fDummy at bits 5-6 and a 1-bit fDummy2 at bit 15. Delphi }
{ 1.0 maps this UINT to TDCB.Flags. }
dcbBinary = $0001;
dcbRtsDisable = $0002;
dcbParity = $0004;
dcbOutxCtsFlow = $0008;
dcbOutxDsrFlow = $0010;
dcbDtrDisable = $0020;
dcbOutX = $0040;
dcbInX = $0080;
dcbPeChar = $0100;
dcbNull = $0200;
dcbChEvt = $0400;
dcbDtrflow = $0800;
dcbRtsflow = $1000;
dcbDtrDisable = $0080;
dcbOutX = $0100;
dcbInX = $0200;
dcbPeChar = $0400;
dcbNull = $0800;
dcbChEvt = $1000;
dcbDtrflow = $2000;
dcbRtsflow = $4000;
{ KPCOMM.DRV EscapeCommFunction extensions (stock COMM.DRV rejects them) }
kpEscKeepDtrOnClose = $4B50; { CloseComm leaves DTR asserted }
{ ----------------------------------------------------------------------- }
@ -170,14 +177,15 @@ begin
else
DCB.Flags := DCB.Flags and not dcbNull;
{ Replacement needs both fParity (check parity) and fPeChar (replace) }
if Length(FParityReplace) > 0 then
begin
DCB.Flags := DCB.Flags or dcbParity;
DCB.Flags := DCB.Flags or dcbParity or dcbPeChar;
DCB.PeChar := FParityReplace[1];
end
else
begin
DCB.Flags := DCB.Flags and not dcbParity;
DCB.Flags := DCB.Flags and not (dcbParity or dcbPeChar);
DCB.PeChar := #0;
end;
@ -197,8 +205,13 @@ begin
ClearCommBreak(FCommId);
FBreakState := False;
end;
EscapeCommFunction(FCommId, CLRDTR);
EscapeCommFunction(FCommId, CLRRTS);
if FHangupOnClose then
begin
EscapeCommFunction(FCommId, CLRDTR);
EscapeCommFunction(FCommId, CLRRTS);
end
else
EscapeCommFunction(FCommId, kpEscKeepDtrOnClose);
CloseComm(FCommId);
FCommId := -1;
end;
@ -219,6 +232,7 @@ begin
FInputMode := imText;
FDTREnable := True;
FRTSEnable := True;
FHangupOnClose := True;
FNullDiscard := False;
FEOFEnable := False;
FParityReplace := '?';
@ -332,8 +346,7 @@ begin
raise Exception.Create('Invalid Settings: ' + FSettings);
end;
DCB.Flags := DCB.Flags or dcbBinary;
DCB.Id := FCommId;
PrepareDCB(DCB);
if SetCommState(DCB) <> 0 then
begin
CloseComm(FCommId);
@ -361,6 +374,20 @@ begin
end;
procedure TKPComm.PrepareDCB(var DCB: TDCB);
{ Common fixups after BuildCommDCB: binary mode, this port's id, and no }
{ modem-line wait timeouts. KPCOMM.DRV honors Cts/Dsr/RlsTimeOut (0 = }
{ ignore the line), so they are pinned here rather than inherited from }
{ whatever USER's default DCB holds; flow control comes from Handshaking. }
begin
DCB.Flags := DCB.Flags or dcbBinary;
DCB.Id := FCommId;
DCB.RlsTimeOut := 0;
DCB.CtsTimeOut := 0;
DCB.DsrTimeOut := 0;
end;
procedure TKPComm.SetBreak(Value: Boolean);
begin
FBreakState := Value;
@ -495,8 +522,7 @@ begin
StrPCopy(Buf, Setting);
if BuildCommDCB(Buf, DCB) <> 0 then
Exit;
DCB.Flags := DCB.Flags or dcbBinary;
DCB.Id := FCommId;
PrepareDCB(DCB);
SetCommState(DCB);
end;
end;

View file

@ -26,6 +26,7 @@ Installs to the **KP** component palette tab.
| InputMode | TInputMode | imText | Data reading mode. imBinary preserves embedded null bytes. |
| DTREnable | Boolean | True | Assert Data Terminal Ready when True. Toggled immediately if port is open. |
| RTSEnable | Boolean | True | Assert Request To Send when True. Toggled immediately if port is open. |
| HangupOnClose | Boolean | True | Drop DTR (and RTS) when the port closes. False keeps DTR asserted through CloseComm -- and through Windows exit -- so a modem stays online for the next program. Requires KPCOMM.DRV (uses its private EscapeCommFunction code $4B50); the stock COMM.DRV ignores it and hangs up. Read at close time, so it can be changed while the port is open. |
| NullDiscard | Boolean | False | Strip null bytes from received data. |
| EOFEnable | Boolean | False | Enable EOF character detection. |
| ParityReplace | string | '?' | Character substituted for parity errors. Empty string disables replacement. |
@ -73,23 +74,24 @@ imBinary Binary mode (preserves embedded nulls)
## DCB Flag Constants
Bit masks for the packed TDCB.Flags field:
Bit masks for the packed TDCB.Flags field. Bits 5-6 (fDummy) and bit 15
(fDummy2) are unused, matching the Windows 3.1 `windows.h` DCB layout:
| Constant | Value | Description |
|---|---|---|
| dcbBinary | $0001 | Binary mode (always set) |
| dcbRtsDisable | $0002 | RTS disabled |
| dcbParity | $0004 | Parity check/replacement |
| dcbParity | $0004 | Parity checking |
| dcbOutxCtsFlow | $0008 | CTS output flow control |
| dcbOutxDsrFlow | $0010 | DSR output flow control |
| dcbDtrDisable | $0020 | DTR disabled |
| dcbOutX | $0040 | XON/XOFF output flow control |
| dcbInX | $0080 | XON/XOFF input flow control |
| dcbPeChar | $0100 | Parity error character replacement |
| dcbNull | $0200 | Null byte stripping |
| dcbChEvt | $0400 | Character event notification |
| dcbDtrflow | $0800 | DTR flow control |
| dcbRtsflow | $1000 | RTS flow control |
| dcbDtrDisable | $0080 | DTR disabled |
| dcbOutX | $0100 | XON/XOFF output flow control |
| dcbInX | $0200 | XON/XOFF input flow control |
| dcbPeChar | $0400 | Parity error character replacement (with dcbParity) |
| dcbNull | $0800 | Null byte stripping |
| dcbChEvt | $1000 | Character event notification |
| dcbDtrflow | $2000 | DTR flow control |
| dcbRtsflow | $4000 | RTS flow control |
## Port Lifecycle
@ -97,12 +99,13 @@ Bit masks for the packed TDCB.Flags field:
1. Create the component and set properties (CommPort, Settings, buffer sizes,
Handshaking, etc.).
2. Set `PortOpen := True`. This calls OpenComm, builds the DCB from
the Settings string, applies flow control, null discard, parity replace,
and asserts DTR/RTS.
the Settings string (with the DCB's CTS/DSR/RLSD wait timeouts zeroed,
so only Handshaking controls flow), applies flow control, null discard,
parity replace, and asserts DTR/RTS.
3. Poll `Input` or call `ReadInputBuf` in the application's main loop.
Set `Output` to send data.
4. Set `PortOpen := False` to close. DTR and RTS are cleared, break is
released, and CloseComm is called.
4. Set `PortOpen := False` to close. Break is released; DTR and RTS are
cleared unless HangupOnClose is False, then CloseComm is called.
5. The destructor closes the port automatically if still open.

631
drv/README.md Normal file
View file

@ -0,0 +1,631 @@
# 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

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,25 @@
// commdrv.h - KPCOMM.DRV -- High-speed COMM.DRV replacement
//
// Types, UART register definitions, port state structure, and prototypes.
// Drop-in replacement for Windows 3.1 stock COMM.DRV with proper 16550
// FIFO management for reliable operation at 57600 and 115200 baud.
// Types, UART/PIC/DPMI/BIOS constants, port state, and the prototypes
// shared between commdrv.c, isr.c, and lpt.c.
//
// Export signatures and semantics follow the Windows 3.1 Device Driver
// Adaptation Guide (DDAG) communication-driver chapter.
#ifndef COMMDRV_H
#define COMMDRV_H
#include <windows.h>
#include <conio.h>
#include <dos.h>
#include <string.h>
// Generate inline code for port I/O, CLI/STI, and far memory helpers
// instead of C runtime calls (matters most inside the ISR).
#pragma intrinsic(_inp, _outp, _disable, _enable, _fmemcpy, _fmemset)
// -----------------------------------------------------------------------
// stdint types for MSVC 1.52 (no stdint.h available)
// stdint/stdbool types for MSVC 1.52 (neither header is available)
// -----------------------------------------------------------------------
#ifndef _STDINT_DEFINED
typedef short int16_t;
@ -23,429 +31,329 @@ typedef signed char int8_t;
#define _STDINT_DEFINED
#endif
#ifndef TRUE
#define TRUE 1
#ifndef UINT16_MAX
#define UINT16_MAX 0xFFFFU
#endif
#ifndef FALSE
#define FALSE 0
#ifndef __bool_true_false_are_defined
typedef uint8_t bool;
#define true 1
#define false 0
#define __bool_true_false_are_defined 1
#endif
// -----------------------------------------------------------------------
// Maximum ports supported
// Debug logging via OutputDebugString (1 = enabled)
// -----------------------------------------------------------------------
#define MAX_PORTS 4
#define DBG_ENABLED 0
// -----------------------------------------------------------------------
// Default buffer sizes
// Ports and queues
// -----------------------------------------------------------------------
#define DEFAULT_RX_SIZE 4096
#define DEFAULT_TX_SIZE 4096
#define MAX_PORTS 4 // COM1-COM4 (GETMAXCOM reports MAX_PORTS - 1)
#define MAX_LPTS 3 // LPT1-LPT3
#define LPT_ID_FLAG 0x80 // cid bit marking a parallel port (DDAG: LPT1 = 0x80)
#define LPT_INDEX_MASK 0x7F // cid bits holding the parallel port index
#define DEFAULT_QUEUE_SIZE 4096 // Queue size used between inicom and setque
#define MIN_QUEUE_SIZE 64 // Smallest queue setque will allocate
#define NO_CHAR (-1) // Empty flowChar/txImmediate slot
#define NOTIFY_OFF (-1) // rx/txNotifyTrigger value disabling that notification
// -----------------------------------------------------------------------
// UART register offsets from base address
// -----------------------------------------------------------------------
#define UART_RBR 0 // Receive Buffer Register (read, DLAB=0)
#define UART_THR 0 // Transmit Holding Register (write, DLAB=0)
#define UART_DLL 0 // Divisor Latch Low (DLAB=1)
#define UART_IER 1 // Interrupt Enable Register (DLAB=0)
#define UART_DLM 1 // Divisor Latch High (DLAB=1)
#define UART_IIR 2 // Interrupt Identification Register (read)
#define UART_FCR 2 // FIFO Control Register (write)
#define UART_LCR 3 // Line Control Register
#define UART_MCR 4 // Modem Control Register
#define UART_LSR 5 // Line Status Register
#define UART_MSR 6 // Modem Status Register
#define UART_SCR 7 // Scratch Register
#define UART_RBR 0 // Receive Buffer Register (read, DLAB=0)
#define UART_THR 0 // Transmit Holding Register (write, DLAB=0)
#define UART_DLL 0 // Divisor Latch Low (DLAB=1)
#define UART_IER 1 // Interrupt Enable Register (DLAB=0)
#define UART_DLM 1 // Divisor Latch High (DLAB=1)
#define UART_IIR 2 // Interrupt Identification Register (read)
#define UART_FCR 2 // FIFO Control Register (write)
#define UART_LCR 3 // Line Control Register
#define UART_MCR 4 // Modem Control Register
#define UART_LSR 5 // Line Status Register
#define UART_MSR 6 // Modem Status Register
// -----------------------------------------------------------------------
// IER bits - Interrupt Enable Register
// -----------------------------------------------------------------------
#define IER_RDA 0x01 // Received Data Available
#define IER_THRE 0x02 // Transmitter Holding Register Empty
#define IER_LSI 0x04 // Line Status Interrupt
#define IER_MSI 0x08 // Modem Status Interrupt
// IER bits
#define IER_RDA 0x01 // Received Data Available
#define IER_THRE 0x02 // Transmitter Holding Register Empty
#define IER_LSI 0x04 // Line Status Interrupt
#define IER_MSI 0x08 // Modem Status Interrupt
#define IER_RX (IER_RDA | IER_LSI | IER_MSI) // Everything except THRE
// -----------------------------------------------------------------------
// IIR bits - Interrupt Identification Register
// -----------------------------------------------------------------------
#define IIR_PENDING 0x01 // 0=interrupt pending, 1=no interrupt
#define IIR_ID_MASK 0x0E // Interrupt ID mask
#define IIR_MSR 0x00 // Modem Status change
#define IIR_THRE 0x02 // Transmitter Holding Register Empty
#define IIR_RDA 0x04 // Received Data Available
#define IIR_LSR 0x06 // Line Status (error/break)
#define IIR_TIMEOUT 0x0C // Character Timeout (16550 FIFO)
#define IIR_FIFO_MASK 0xC0 // FIFO enabled bits
// IIR bits
#define IIR_NO_INT 0x01 // Set when no interrupt is pending
#define IIR_ID_MASK 0x0E // Interrupt source mask
#define IIR_MSR 0x00 // Modem status change
#define IIR_THRE 0x02 // Transmitter holding register empty
#define IIR_RDA 0x04 // Received data available
#define IIR_LSR 0x06 // Line status (error/break)
#define IIR_TIMEOUT 0x0C // Character timeout (16550 FIFO)
#define IIR_FIFO_MASK 0xC0 // Both set = 16550A FIFOs enabled
// -----------------------------------------------------------------------
// FCR bits - FIFO Control Register (write-only)
// -----------------------------------------------------------------------
// FCR bits
#define FCR_ENABLE 0x01 // Enable FIFOs
#define FCR_RX_RESET 0x02 // Reset RX FIFO
#define FCR_TX_RESET 0x04 // Reset TX FIFO
#define FCR_DMA_MODE 0x08 // DMA mode select
#define FCR_TRIG_1 0x00 // RX trigger level: 1 byte
#define FCR_TRIG_4 0x40 // RX trigger level: 4 bytes
#define FCR_TRIG_8 0x80 // RX trigger level: 8 bytes
#define FCR_TRIG_14 0xC0 // RX trigger level: 14 bytes
#define FCR_TRIG_SHIFT 6 // RX trigger code in bits 7:6 (0-3 = 1/4/8/14 bytes)
// LCR bits
#define LCR_STB 0x04 // Extra stop bits (1.5 with 5 data bits, else 2)
#define LCR_PEN 0x08 // Parity Enable
#define LCR_EPS 0x10 // Even Parity Select
#define LCR_SPAR 0x20 // Stick Parity
#define LCR_SBRK 0x40 // Set Break
#define LCR_DLAB 0x80 // Divisor Latch Access Bit
// MCR bits
#define MCR_DTR 0x01 // Data Terminal Ready
#define MCR_RTS 0x02 // Request To Send
#define MCR_OUT2 0x08 // Output 2 (gates the UART interrupt line onto the bus)
// LSR bits
#define LSR_DR 0x01 // Data Ready
#define LSR_OE 0x02 // Overrun Error
#define LSR_PE 0x04 // Parity Error
#define LSR_FE 0x08 // Framing Error
#define LSR_BI 0x10 // Break Interrupt
#define LSR_THRE 0x20 // Transmitter holding register (FIFO) empty
#define LSR_TEMT 0x40 // Transmitter completely empty (shift register too)
#define LSR_ERRORS (LSR_OE | LSR_PE | LSR_FE | LSR_BI)
// MSR bits
#define MSR_DCTS 0x01 // Delta CTS
#define MSR_DDSR 0x02 // Delta DSR
#define MSR_TERI 0x04 // Trailing Edge Ring Indicator
#define MSR_DDCD 0x08 // Delta DCD
#define MSR_CTS 0x10 // Clear To Send
#define MSR_DSR 0x20 // Data Set Ready
#define MSR_DCD 0x80 // Data Carrier Detect (RLSD)
// UART geometry and baud rates
#define FIFO_DEPTH 16 // 16550A TX/RX FIFO size
#define UART_PRESENT_LCR 0x1B // LCR test pattern for presence detection
#define UART_ABSENT 0xFF // Register reads from an empty I/O address float high
#define UART_CLOCK_DIV 115200UL // 1.8432 MHz / 16
#define MIN_RAW_BAUD 2 // DDAG: raw rates are 2..0xFEFF
#define CBR_INDEX_MASK 0xFF00 // High byte 0xFF marks a CBR_* index
#define KPCBR_115200 0xFF24 // KPCOMM extension: CBR-style index for 115200
#define MIN_BYTE_SIZE 5 // Data bits; LCR word length = ByteSize - 5
#define MAX_BYTE_SIZE 8
// -----------------------------------------------------------------------
// LCR bits - Line Control Register
// PIC, DPMI, and BIOS
// -----------------------------------------------------------------------
#define LCR_WLS_MASK 0x03 // Word Length Select mask
#define LCR_WLS_5 0x00 // 5 data bits
#define LCR_WLS_6 0x01 // 6 data bits
#define LCR_WLS_7 0x02 // 7 data bits
#define LCR_WLS_8 0x03 // 8 data bits
#define LCR_STB 0x04 // Number of Stop Bits (0=1, 1=2)
#define LCR_PEN 0x08 // Parity Enable
#define LCR_EPS 0x10 // Even Parity Select
#define LCR_SPAR 0x20 // Stick Parity
#define LCR_SBRK 0x40 // Set Break
#define LCR_DLAB 0x80 // Divisor Latch Access Bit
#define PIC1_CMD 0x20 // Master PIC command port
#define PIC1_DATA 0x21 // Master PIC mask port
#define PIC2_CMD 0xA0 // Slave PIC command port
#define PIC2_DATA 0xA1 // Slave PIC mask port
#define PIC_EOI 0x20 // Non-specific End Of Interrupt
#define PIC_READ_IRR 0x0A // OCW3: command port reads the request register (default)
#define PIC_READ_ISR 0x0B // OCW3: command port reads the in-service register
#define IRQ_COUNT 16 // IRQ 0-15
#define IRQ_SPURIOUS_PIC1 7 // Spurious interrupts from the master PIC appear as IRQ 7
#define IRQ_SPURIOUS_PIC2 15 // ...and from the slave PIC as IRQ 15
#define IRQ_SLAVE_FIRST 8 // First IRQ on the slave PIC
#define IRQ_CASCADE 2 // ISA "IRQ2" is delivered as IRQ9 on AT systems
#define IRQ_CASCADE_TARGET 9
#define PM_VECTOR_MASTER 0x08 // DPMI protected-mode vector for IRQ0
#define PM_VECTOR_SLAVE 0x70 // DPMI protected-mode vector for IRQ8
#define DPMI_INT 0x31
#define DPMI_SEG_TO_DESC 0x0002 // Map real-mode segment to selector
#define DPMI_GET_PM_VECTOR 0x0204
#define DPMI_SET_PM_VECTOR 0x0205
#define BIOS_DATA_SEG 0x0040
#define BDA_COM_BASES 0x0000 // COM1-COM4 base addresses (words)
#define BDA_LPT_BASES 0x0008 // LPT1-LPT3 base addresses (words)
#define BDA_TICK_COUNT 0x006C // Timer ticks since midnight (low word used, ~18.2 Hz)
#define MS_PER_BIOS_TICK 55 // Milliseconds per BIOS timer tick (rounded)
// -----------------------------------------------------------------------
// MCR bits - Modem Control Register
// -----------------------------------------------------------------------
#define MCR_DTR 0x01 // Data Terminal Ready
#define MCR_RTS 0x02 // Request To Send
#define MCR_OUT1 0x04 // Output 1
#define MCR_OUT2 0x08 // Output 2 (master interrupt enable)
#define MCR_LOOP 0x10 // Loopback mode
// -----------------------------------------------------------------------
// LSR bits - Line Status Register
// -----------------------------------------------------------------------
#define LSR_DR 0x01 // Data Ready
#define LSR_OE 0x02 // Overrun Error
#define LSR_PE 0x04 // Parity Error
#define LSR_FE 0x08 // Framing Error
#define LSR_BI 0x10 // Break Interrupt
#define LSR_THRE 0x20 // Transmitter Holding Register Empty
#define LSR_TEMT 0x40 // Transmitter Empty (shift register too)
#define LSR_FIFO 0x80 // Error in RX FIFO (16550)
// -----------------------------------------------------------------------
// MSR bits - Modem Status Register
// -----------------------------------------------------------------------
#define MSR_DCTS 0x01 // Delta CTS
#define MSR_DDSR 0x02 // Delta DSR
#define MSR_TERI 0x04 // Trailing Edge Ring Indicator
#define MSR_DDCD 0x08 // Delta DCD
#define MSR_CTS 0x10 // Clear To Send
#define MSR_DSR 0x20 // Data Set Ready
#define MSR_RI 0x40 // Ring Indicator
#define MSR_DCD 0x80 // Data Carrier Detect
// -----------------------------------------------------------------------
// PIC constants
// -----------------------------------------------------------------------
#define PIC_CMD 0x20 // PIC command port
#define PIC_DATA 0x21 // PIC data (mask) port
#define PIC_EOI 0x20 // End of Interrupt command
// -----------------------------------------------------------------------
// Standard port base addresses and IRQs
// Default COM port resources (SYSTEM.INI [386Enh] COMnBase/COMnIRQ override
// only when the BIOS data area has no entry, per DDAG)
// -----------------------------------------------------------------------
#define COM1_BASE 0x03F8
#define COM2_BASE 0x02F8
#define COM3_BASE 0x03E8
#define COM4_BASE 0x02E8
#define COM1_IRQ 4
#define COM2_IRQ 3
#define COM3_IRQ 4
#define COM4_IRQ 3
#define COM1_IRQ 4
#define COM2_IRQ 3
#define COM3_IRQ 4
#define COM4_IRQ 3
// -----------------------------------------------------------------------
// Baud rate divisor (115200 / baud)
// Return codes not defined by windows.h
// -----------------------------------------------------------------------
#define BAUD_DIVISOR_BASE 115200UL
#define DRV_ERROR (-1) // Generic failure for exports without a documented code
#define RECCOM_NO_DATA (-2) // DDAG reccom: no data available
#define TRMCOM_TIMEOUT (-2) // DDAG trmcom: transmit queue did not drain
#define CTX_BUSY 0x4000 // DDAG ctx: immediate character already pending
#define SNDCOM_FAILED 1 // DDAG sndcom: any nonzero value is an error
#define BASEIRQ_NO_PORT MAKELONG(0, 0xFFFF) // DDAG GETBASEIRQ: high word -1 = no such port
// cflush queue selectors (DDAG: 0 = transmit, 1 = receive)
#define FLUSH_TX 0
#define FLUSH_RX 1
// -----------------------------------------------------------------------
// CBR_* baud rate index constants
//
// The 16-bit DCB BaudRate field is a UINT (16 bits). Since 115200
// exceeds 65535, high baud rates use CBR_* index constants instead
// of raw values. Values >= 0xFF00 are indices, not raw rates.
// KPCOMM EscapeCommFunction extensions (stock drivers reject them)
// -----------------------------------------------------------------------
// Most CBR_* constants defined by windows.h; add any missing ones
#ifndef CBR_110
#define CBR_110 0xFF10
#define CBR_300 0xFF11
#define CBR_600 0xFF12
#define CBR_1200 0xFF13
#define CBR_2400 0xFF14
#define CBR_4800 0xFF15
#define CBR_9600 0xFF16
#endif
#ifndef CBR_14400
#define CBR_14400 0xFF17
#endif
#ifndef CBR_19200
#define CBR_19200 0xFF18
#endif
#ifndef CBR_38400
#define CBR_38400 0xFF1B
#endif
#ifndef CBR_56000
#define CBR_56000 0xFF1F
#endif
#ifndef CBR_115200
#define CBR_115200 0xFF24
#endif
#define KPESC_KEEP_DTR_ON_CLOSE 0x4B50 // CloseComm leaves DTR asserted (connection stays up)
#define KPESC_DROP_DTR_ON_CLOSE 0x4B51 // CloseComm drops DTR (the default; hangs up)
// -----------------------------------------------------------------------
// 16550 FIFO depth
// Flow control state bits
// -----------------------------------------------------------------------
#define FIFO_DEPTH 16
#define TX_HOLD_XOFF 0x01 // Remote sent XOFF (fOutX)
#define TX_HOLD_CTS 0x02 // CTS low (fOutxCtsFlow or CtsTimeout INFINITE)
#define TX_HOLD_DSR 0x04 // DSR low (fOutxDsrFlow or DsrTimeout INFINITE)
#define TX_HOLD_RLSD 0x08 // RLSD/DCD low (RlsTimeout INFINITE)
#define TX_HOLD_HW (TX_HOLD_CTS | TX_HOLD_DSR | TX_HOLD_RLSD)
// -----------------------------------------------------------------------
// Comm error flags (CE_* -- most defined by windows.h, fill any gaps)
// -----------------------------------------------------------------------
#ifndef CE_RXOVER
#define CE_RXOVER 0x0001
#endif
#ifndef CE_OVERRUN
#define CE_OVERRUN 0x0002
#endif
#ifndef CE_RXPARITY
#define CE_RXPARITY 0x0004
#endif
#ifndef CE_FRAME
#define CE_FRAME 0x0008
#endif
#ifndef CE_BREAK
#define CE_BREAK 0x0010
#endif
#ifndef CE_TXFULL
#define CE_TXFULL 0x0020
#endif
#ifndef CE_MODE
#define CE_MODE 0x8000
#endif
// -----------------------------------------------------------------------
// Comm event flags (EV_* -- matches Windows SDK definitions)
// -----------------------------------------------------------------------
#define EV_RXCHAR 0x0001 // Any character received
#define EV_RXFLAG 0x0002 // Event character received
#define EV_TXEMPTY 0x0004 // TX buffer empty
#define EV_CTS 0x0008 // CTS changed
#define EV_DSR 0x0010 // DSR changed
#define EV_RLSD 0x0020 // DCD/RLSD changed
#define EV_BREAK 0x0040 // Break received
#define EV_ERR 0x0080 // Line status error
#define EV_RING 0x0100 // Ring indicator
// -----------------------------------------------------------------------
// CN_* notification codes (lParam for WM_COMMNOTIFY)
// -----------------------------------------------------------------------
#ifndef CN_RECEIVE
#define CN_RECEIVE 0x0001
#define CN_TRANSMIT 0x0002
#define CN_EVENT 0x0004
#endif
// -----------------------------------------------------------------------
// WM_COMMNOTIFY message
// -----------------------------------------------------------------------
#ifndef WM_COMMNOTIFY
#define WM_COMMNOTIFY 0x0044
#endif
// -----------------------------------------------------------------------
// Handshaking modes
// -----------------------------------------------------------------------
#define HS_NONE 0
#define HS_XONXOFF 1
#define HS_RTSCTS 2
#define HS_BOTH 3
// -----------------------------------------------------------------------
// EscapeCommFunction codes
//
// SETXON through RESETDEV defined by windows.h.
// SETBREAK/CLRBREAK are our extensions for routing through cextfcn.
// -----------------------------------------------------------------------
#ifndef SETXON
#define SETXON 1
#define SETXOFF 2
#define SETRTS 3
#define CLRRTS 4
#define SETDTR 5
#define CLRDTR 6
#define RESETDEV 7
#endif
#define ESC_SETBREAK 8
#define ESC_CLRBREAK 9
// -----------------------------------------------------------------------
// Flush queue selectors
// -----------------------------------------------------------------------
#define FLUSH_RX 0
#define FLUSH_TX 1
#define RX_THROTTLE_XOFF 0x01 // We sent XOFF (fInX)
#define RX_THROTTLE_RTS 0x02 // We dropped RTS (fRtsflow)
#define RX_THROTTLE_DTR 0x04 // We dropped DTR (fDtrflow)
// -----------------------------------------------------------------------
// Stock COMM.DRV compatible ComDEB structure
//
// SetCommEventMask (cevt) returns a FAR pointer to offset 0 of this
// structure. Third-party code (ProComm COMMTASK.DLL, etc.) accesses
// fields at known offsets -- most importantly offset 35 (MSR shadow)
// per Microsoft KB Q101417. Our layout must match the stock driver.
// cevt (SetCommEventMask) returns a FAR pointer to offset 0. Third-party
// code reads the MSR shadow at offset 35 (KB Q101417; Wine uses the same
// offset). The queue fields are the driver's live ring-buffer state, so
// readers at +8/+18 always see current counts without any syncing.
// -----------------------------------------------------------------------
typedef struct {
uint16_t evtWord; // +0: Accumulated event flags (EV_*)
uint16_t evtMask; // +2: Event enable mask
uint16_t qInAddr; // +4: RX queue offset (compat, unused)
uint16_t qInSize; // +6: RX buffer size
uint16_t qInCount; // +8: RX bytes in buffer
uint16_t qInHead; // +10: RX write position
uint16_t qInTail; // +12: RX read position
uint16_t qOutAddr; // +14: TX queue offset (compat, unused)
uint16_t qOutSize; // +16: TX buffer size
uint16_t qOutCount; // +18: TX bytes in buffer
uint16_t qOutHead; // +20: TX write position
uint16_t qOutTail; // +22: TX read position
uint16_t port; // +24: UART base I/O address
uint16_t baudRate; // +26: Current baud rate
uint8_t lcrShadow; // +28: LCR shadow
uint8_t mcrShadow; // +29: MCR shadow
uint8_t ierShadow; // +30: IER shadow
uint8_t commErr; // +31: Error flags (low byte)
uint8_t flags1; // +32: Internal flags
uint8_t flags2; // +33: More internal flags
uint8_t recvTrigger; // +34: FIFO trigger level
uint8_t msrShadow; // +35: MSR shadow (documented KB Q101417)
uint8_t padding[4]; // +36: Pad to 40 bytes
volatile uint16_t evtWord; // +0: Accumulated events (only those in evtMask)
uint16_t evtMask; // +2: Events to record (set by cevt)
uint16_t reserved4; // +4: Stock RX queue address (unused)
uint16_t rxSize; // +6: RX ring buffer capacity
volatile uint16_t rxCount; // +8: RX bytes queued (ISR adds, app removes)
uint16_t rxHead; // +10: RX write index (ISR)
uint16_t rxTail; // +12: RX read index (app)
uint16_t reserved14; // +14: Stock TX queue address (unused)
uint16_t txSize; // +16: TX ring buffer capacity
volatile uint16_t txCount; // +18: TX bytes queued (app adds, ISR removes)
uint16_t txHead; // +20: TX write index (app)
uint16_t txTail; // +22: TX read index (ISR)
uint16_t baseAddr; // +24: UART base I/O address
uint16_t baudRate; // +26: Mirror of dcb.BaudRate for third-party readers (written only by applyDcb)
uint8_t lcrShadow; // +28: Last LCR value written (including break bit)
volatile uint8_t mcrShadow; // +29: Last MCR value written
volatile uint8_t ierShadow; // +30: Last IER value written
uint8_t reserved31; // +31
uint8_t reserved32; // +32
uint8_t reserved33; // +33
uint8_t fifoTrigger; // +34: RX FIFO trigger level (FCR_TRIG_* bits)
volatile uint8_t msrShadow; // +35: Last MSR value read (KB Q101417)
uint8_t reserved36[4]; // +36: Pad to 40 bytes
} ComDebT;
// -----------------------------------------------------------------------
// Port state structure
//
// One per COM port. Accessed from both application context (reccom,
// sndcom, etc.) and ISR context (handleRx, handleTx).
// Fields shared between contexts are protected by _disable()/_enable().
// Queue definition block passed to setque (DDAG "qdb"). USER allocates
// pqRx/pqTx, but KPCOMM keeps its own page-locked buffers and uses only
// the requested sizes.
// -----------------------------------------------------------------------
typedef struct {
// Stock-compatible ComDEB -- must be at offset 0.
// cevt (SetCommEventMask) returns a FAR pointer to this.
// Third-party code reads msrShadow at offset 35 (KB Q101417).
ComDebT comDeb;
uint8_t FAR *rxQueue; // pqRx
int16_t rxSize; // cbqRx
uint8_t FAR *txQueue; // pqTx
int16_t txSize; // cbqTx
} QdbT;
// Hardware identification (from SYSTEM.INI or defaults)
uint16_t baseAddr; // UART base I/O address (e.g. 0x03F8 for COM1)
uint8_t irq; // IRQ number (3 for COM2/4, 4 for COM1/3)
int16_t commId; // Port index (0=COM1, 1=COM2, 2=COM3, 3=COM4)
uint8_t isOpen; // Nonzero while port is open (guards ISR dispatch)
uint8_t is16550; // Nonzero if 16550 FIFO detected at init
uint8_t fifoEnabled; // Nonzero if FIFO use enabled (COMnFIFO in SYSTEM.INI)
uint8_t fifoTrigger; // RX FIFO trigger level FCR bits (FCR_TRIG_*)
// GlobalAlloc'd, page-locked ring buffer storage
typedef struct {
HGLOBAL handle;
uint8_t FAR *data;
} BufferT;
// Receive ring buffer (GlobalAlloc'd GMEM_FIXED for ISR stability)
HGLOBAL rxBufH; // GlobalAlloc handle (for GlobalFree on close)
uint8_t FAR *rxBuf; // Far pointer to buffer data
uint16_t rxSize; // Buffer capacity in bytes
uint16_t rxHead; // Write position -- ISR increments
uint16_t rxTail; // Read position -- reccom increments
uint16_t rxCount; // Bytes in buffer (ISR increments, reccom decrements)
// Transmit ring buffer (GlobalAlloc'd GMEM_FIXED for ISR stability)
HGLOBAL txBufH; // GlobalAlloc handle (for GlobalFree on close)
uint8_t FAR *txBuf; // Far pointer to buffer data
uint16_t txSize; // Buffer capacity in bytes
uint16_t txHead; // Write position -- sndcom increments
uint16_t txTail; // Read position -- ISR (handleTx) increments
uint16_t txCount; // Bytes in buffer (sndcom increments, ISR decrements)
// Serial parameters (cached from DCB at inicom/setcom time)
uint16_t baudRate; // Baud rate (raw or CBR_* index for >32767)
uint8_t byteSize; // Data bits per character (5-8)
uint8_t parity; // Parity mode (NOPARITY, ODDPARITY, EVENPARITY, ...)
uint8_t stopBits; // Stop bits (ONESTOPBIT, TWOSTOPBITS)
// Flow control state (managed by ISR and application code)
uint8_t hsMode; // Handshaking mode (HS_NONE/XONXOFF/RTSCTS/BOTH)
uint8_t txStopped; // Nonzero = TX halted (received XOFF or CTS dropped)
uint8_t rxStopped; // Nonzero = we sent XOFF or dropped RTS
uint8_t xonChar; // XON character to send/recognize (default 0x11 DC1)
uint8_t xoffChar; // XOFF character to send/recognize (default 0x13 DC3)
uint16_t xoffLim; // Assert flow control when rxCount > rxSize - xoffLim
uint16_t xonLim; // Release flow control when rxCount < xonLim
// Modem control line shadow (tracks EscapeCommFunction calls)
uint8_t dtrState; // Nonzero = DTR is asserted
uint8_t rtsState; // Nonzero = RTS is asserted (when not flow-controlled)
// Error accumulator (sticky CE_* bits, cleared by stacom/GetCommError)
uint16_t errorFlags; // Accumulated CE_RXOVER, CE_OVERRUN, CE_FRAME, etc.
// WM_COMMNOTIFY event notification (stored for API compat, not used)
HWND hwndNotify; // Target window for PostMessage (0=disabled)
int16_t rxNotifyThresh; // CN_RECEIVE threshold (-1=disabled)
int16_t txNotifyThresh; // CN_TRANSMIT threshold (-1=disabled)
// ISR chaining and PIC management
void (FAR *prevIsr)(void); // Previous ISR vector (restored on unhook)
uint8_t irqMask; // PIC mask bit for this IRQ (1 << irq)
uint8_t breakState; // Nonzero while break signal is active on line
// Priority transmit (XON/XOFF flow control characters)
int16_t txImmediate; // -1=none, 0..255=char to send before buffered data
// Full DCB copy (returned by getdcb, updated by setcom)
DCB dcb;
// -----------------------------------------------------------------------
// COM port state -- one per port, shared by application and ISR context.
// Fields the ISR also writes are volatile; multi-field updates from
// application context are made with interrupts disabled.
// -----------------------------------------------------------------------
typedef struct {
ComDebT comDeb; // Must be first: cevt returns &comDeb.evtWord
DCB dcb; // Current settings (getdcb); source of truth for line and flow parameters
BufferT rxBuf; // RX ring buffer storage
BufferT txBuf; // TX ring buffer storage
HWND hwndNotify; // WM_COMMNOTIFY target (NULL = notifications off)
int16_t rxNotifyTrigger; // CN_RECEIVE threshold (-1 = off)
int16_t txNotifyTrigger; // CN_TRANSMIT threshold (-1 = off)
volatile int16_t flowChar; // XON/XOFF to send ahead of everything (NO_CHAR = none)
volatile int16_t txImmediate; // ctx() character (NO_CHAR = none)
volatile uint16_t errorFlags; // Sticky CE_* bits, cleared by stacom
uint8_t irq; // IRQ line (0-15)
volatile bool isOpen; // Port open (guards ISR dispatch)
bool useFifo; // 16550A detected and COMnFIFO enabled
volatile uint8_t txHold; // TX_HOLD_* reasons transmission is paused
volatile uint8_t rxThrottle; // RX_THROTTLE_* actions taken to pause the remote
bool dtrOn; // DTR wanted by DCB/EscapeCommFunction (flow control may hold it low)
bool rtsOn; // RTS wanted by DCB/EscapeCommFunction (flow control may hold it low)
bool keepDtrOnClose; // Close leaves DTR up (COMnKeepDTR, KPESC_*_DTR_ON_CLOSE)
volatile bool eofSeen; // EofChar received in non-binary mode (CSTF_EOF)
volatile uint8_t notifySent; // CN_* notifications posted and not yet re-armed
} PortStateT;
// -----------------------------------------------------------------------
// Global port state array (one entry per COM port, indexed by commId)
// -----------------------------------------------------------------------
extern PortStateT ports[MAX_PORTS];
// Parallel port state (polled output through BIOS INT 17h)
typedef struct {
volatile uint16_t evtWord; // cevt target (printers raise no events)
uint16_t evtMask; // Set by cevt
uint16_t errorFlags; // CE_PTO/CE_IOE/CE_DNS/CE_OOP, cleared by stacom
bool isOpen;
DCB dcb; // Settings handed to inicom/setcom (getdcb)
} LptStateT;
// ISR hit counter for diagnostics (incremented in isr4, wraps at 65535)
extern volatile uint16_t isrHitCount;
typedef BOOL (FAR PASCAL *PostMessageProcT)(HWND, UINT, WPARAM, LPARAM);
// -----------------------------------------------------------------------
// Exported function prototypes (COMM.DRV API)
//
// These use the Windows COMM.DRV calling convention: FAR PASCAL
// Shared globals
// -----------------------------------------------------------------------
int16_t FAR PASCAL _export inicom(DCB FAR *dcb);
int16_t FAR PASCAL _export setcom(DCB FAR *dcb);
int16_t FAR PASCAL _export setque(int16_t commId, int16_t rxSize, int16_t txSize);
int16_t FAR PASCAL _export reccom(int16_t commId, void FAR *buf, int16_t len);
int16_t FAR PASCAL _export sndcom(int16_t commId, void FAR *buf, int16_t len);
int16_t FAR PASCAL _export ctx(int16_t commId, int16_t ch);
int16_t FAR PASCAL _export trmcom(int16_t commId);
int16_t FAR PASCAL _export stacom(int16_t commId, COMSTAT FAR *stat);
int32_t FAR PASCAL _export cevt(int16_t commId, int16_t evtMask);
uint16_t FAR PASCAL _export cevtget(int16_t commId, int16_t evtMask);
int16_t FAR PASCAL _export cextfcn(int16_t commId, int16_t func);
int16_t FAR PASCAL _export cflush(int16_t commId, int16_t queue);
int16_t FAR PASCAL _export csetbrk(int16_t commId);
int16_t FAR PASCAL _export cclrbrk(int16_t commId);
DCB FAR * FAR PASCAL _export getdcb(int16_t commId);
void FAR PASCAL _export suspendOpenCommPorts(void);
void FAR PASCAL _export reactivateOpenCommPorts(void);
int16_t FAR PASCAL _export commWriteString(int16_t commId, void FAR *buf, int16_t len);
int16_t FAR PASCAL _export readCommString(int16_t commId, void FAR *buf, int16_t len);
int16_t FAR PASCAL _export enableNotification(int16_t commId, HWND hwnd, int16_t rxThresh, int16_t txThresh);
int FAR PASCAL _export commNotifyWndProc(void);
extern PortStateT ports[MAX_PORTS]; // commdrv.c
extern volatile bool portsSuspended; // commdrv.c: true between Suspend/ReactivateOpenCommPorts
extern PostMessageProcT postMessageProc; // isr.c: resolved from USER by enableNotification
// -----------------------------------------------------------------------
// ISR prototypes (isr.c)
// commdrv.c
// -----------------------------------------------------------------------
uint16_t readBiosWord(uint16_t offset);
#if DBG_ENABLED
void dbgHex16(const char FAR *label, uint16_t val);
void dbgStr(const char FAR *msg);
#else
#define dbgHex16(label, val) ((void)0)
#define dbgStr(msg) ((void)0)
#endif
// -----------------------------------------------------------------------
// isr.c (functions marked "CLI" require interrupts disabled)
// -----------------------------------------------------------------------
void checkNotify(PortStateT *port); // CLI
int16_t hookIsr(PortStateT *port);
void reactivateIrqs(void);
void refreshNotify(PortStateT *port, uint8_t acked); // CLI
void releaseRxFlow(PortStateT *port); // CLI
void startTx(PortStateT *port); // CLI
void unhookIsr(PortStateT *port);
void isrDispatch(PortStateT *port);
void updateRxFlow(PortStateT *port); // CLI
void updateTxHold(PortStateT *port); // CLI
void writeIer(PortStateT *port, uint8_t ier); // CLI
void writeMcr(PortStateT *port); // CLI
// -----------------------------------------------------------------------
// Internal helpers
// lpt.c
// -----------------------------------------------------------------------
int16_t detect16550(uint16_t baseAddr);
void applyBaudRate(PortStateT *port, uint16_t baud);
void applyLineParams(PortStateT *port, uint8_t byteSize, uint8_t parity, uint8_t stopBits);
void enableFifo(PortStateT *port);
void primeTx(PortStateT *port);
LptStateT *getOpenLpt(int16_t commId);
bool isLptId(int16_t commId);
int32_t lptBaseIrq(int16_t commId);
int16_t lptClose(int16_t commId);
void lptCloseAll(void);
int16_t lptMaxId(void);
int16_t lptOpen(const DCB FAR *dcb);
void lptReset(LptStateT *lpt);
int16_t lptWrite(LptStateT *lpt, const uint8_t FAR *buf, int16_t len);
#endif // COMMDRV_H

1120
drv/isr.c

File diff suppressed because it is too large Load diff

217
drv/lpt.c Normal file
View file

@ -0,0 +1,217 @@
// lpt.c - Parallel port support for KPCOMM.DRV
//
// The stock COMM.DRV also serves LPT1-LPT3 (cid 0x80-0x82), and Windows
// routes direct-to-port printing through it. Output is polled through
// BIOS INT 17h, which Windows reflects to the real-mode BIOS in both
// standard and enhanced mode. Printers have no input, events, or line
// settings; those calls succeed without doing anything.
#include "commdrv.h"
#define BIOS_PRINTER_INT 0x17
#define PRN_WRITE 0x00 // AH: print the character in AL
#define PRN_INIT 0x01 // AH: initialize (reset) the printer
#define PRN_TIMEOUT 0x01 // Status: timed out
#define PRN_IO_ERROR 0x08 // Status: I/O error
#define PRN_SELECTED 0x10 // Status: printer selected (online)
#define PRN_OUT_OF_PAPER 0x20 // Status: out of paper
#define PRN_FAILED (PRN_TIMEOUT | PRN_IO_ERROR | PRN_OUT_OF_PAPER)
#define LPT1_IRQ 7 // Conventional IRQs reported by GETBASEIRQ
#define LPTN_IRQ 5
// -----------------------------------------------------------------------
// Prototypes (public functions are declared in commdrv.h)
// -----------------------------------------------------------------------
static uint8_t biosPrinter(uint8_t func, uint8_t data, uint16_t index);
static uint16_t lptBase(uint16_t index);
static uint16_t lptErrors(uint8_t status);
static LptStateT lpts[MAX_LPTS];
// -----------------------------------------------------------------------
// biosPrinter - Call BIOS INT 17h; returns the printer status byte
// -----------------------------------------------------------------------
static uint8_t biosPrinter(uint8_t func, uint8_t data, uint16_t index) {
uint8_t status;
_asm {
mov ah, func
mov al, data
mov dx, index
int BIOS_PRINTER_INT
mov status, ah
}
return status;
}
// -----------------------------------------------------------------------
// getOpenLpt - Parallel port state for an open LPT cid, or NULL
// -----------------------------------------------------------------------
LptStateT *getOpenLpt(int16_t commId) {
LptStateT *lpt;
if (!isLptId(commId)) {
return NULL;
}
lpt = &lpts[commId & LPT_INDEX_MASK];
return lpt->isOpen ? lpt : NULL;
}
// -----------------------------------------------------------------------
// isLptId - True if commId names LPT1-LPT3
// -----------------------------------------------------------------------
bool isLptId(int16_t commId) {
return ((uint16_t)commId & ~LPT_INDEX_MASK) == LPT_ID_FLAG && ((uint16_t)commId & LPT_INDEX_MASK) < MAX_LPTS;
}
// -----------------------------------------------------------------------
// lptBase - I/O address of a parallel port from the BIOS (0 = none)
// -----------------------------------------------------------------------
static uint16_t lptBase(uint16_t index) {
return readBiosWord(BDA_LPT_BASES + index * sizeof(uint16_t));
}
// -----------------------------------------------------------------------
// lptBaseIrq - GETBASEIRQ for a parallel port
// -----------------------------------------------------------------------
int32_t lptBaseIrq(int16_t commId) {
uint16_t index;
uint16_t base;
if (!isLptId(commId)) {
return BASEIRQ_NO_PORT;
}
index = commId & LPT_INDEX_MASK;
base = lptBase(index);
if (base == 0) {
return BASEIRQ_NO_PORT;
}
return MAKELONG(base, (index == 0) ? LPT1_IRQ : LPTN_IRQ);
}
// -----------------------------------------------------------------------
// lptClose - Close a parallel port (trmcom); CE_MODE if it was not open
// -----------------------------------------------------------------------
int16_t lptClose(int16_t commId) {
LptStateT *lpt;
lpt = getOpenLpt(commId);
if (!lpt) {
return (int16_t)CE_MODE;
}
lpt->isOpen = false;
return 0;
}
// -----------------------------------------------------------------------
// lptCloseAll - Close every parallel port (WEP)
// -----------------------------------------------------------------------
void lptCloseAll(void) {
uint16_t i;
for (i = 0; i < MAX_LPTS; i++) {
lpts[i].isOpen = false;
}
}
// -----------------------------------------------------------------------
// lptErrors - CE_* flags for a failing printer status
// -----------------------------------------------------------------------
static uint16_t lptErrors(uint8_t status) {
uint16_t errors;
errors = 0;
if (status & PRN_TIMEOUT) {
errors |= CE_PTO;
}
if (status & PRN_IO_ERROR) {
errors |= CE_IOE;
}
if (!(status & PRN_SELECTED)) {
errors |= CE_DNS;
}
if (status & PRN_OUT_OF_PAPER) {
errors |= CE_OOP;
}
return errors;
}
// -----------------------------------------------------------------------
// lptMaxId - GETMAXLPT: highest LPT cid the BIOS reports (LPT1 if none)
// -----------------------------------------------------------------------
int16_t lptMaxId(void) {
uint16_t count;
count = 0;
while (count < MAX_LPTS && lptBase(count) != 0) {
count++;
}
return (int16_t)(LPT_ID_FLAG + ((count > 0) ? count - 1 : 0));
}
// -----------------------------------------------------------------------
// lptOpen - Open a parallel port (inicom); returns 0 or IE_*
// -----------------------------------------------------------------------
int16_t lptOpen(const DCB FAR *dcb) {
LptStateT *lpt;
uint16_t index;
if (!isLptId(dcb->Id)) {
return IE_BADID;
}
index = dcb->Id & LPT_INDEX_MASK;
lpt = &lpts[index];
if (lpt->isOpen) {
return IE_OPEN;
}
if (lptBase(index) == 0) {
return IE_HARDWARE;
}
_fmemset(lpt, 0, sizeof(LptStateT));
_fmemcpy(&lpt->dcb, dcb, sizeof(DCB));
lpt->isOpen = true;
return 0;
}
// -----------------------------------------------------------------------
// lptReset - Pulse the printer's reset line (RESETDEV)
// -----------------------------------------------------------------------
void lptReset(LptStateT *lpt) {
(void)biosPrinter(PRN_INIT, 0, (uint16_t)(lpt - lpts));
}
// -----------------------------------------------------------------------
// lptWrite - Print up to len bytes; returns the number accepted
//
// Stops at the first failure and records it for stacom.
// -----------------------------------------------------------------------
int16_t lptWrite(LptStateT *lpt, const uint8_t FAR *buf, int16_t len) {
uint16_t index;
int16_t i;
uint8_t status;
index = (uint16_t)(lpt - lpts);
for (i = 0; i < len; i++) {
status = biosPrinter(PRN_WRITE, buf[i], index);
if (status & PRN_FAILED) {
lpt->errorFlags |= lptErrors(status);
break;
}
}
return i;
}

View file

@ -6,17 +6,25 @@
# Prerequisites:
# - MSVC 1.52 (cl, link in PATH)
#
# Output is KPCOMM.DRV -- drop-in replacement for stock Windows 3.1 driver.
# Output is KPCOMM.DRV -- drop-in replacement for the stock Windows 3.1 driver.
# Full documentation: README.md (features, settings, API) and ring0.md.
#
# Install:
# 1. Copy KPCOMM.DRV to \WINDOWS\SYSTEM
# 2. Edit SYSTEM.INI [boot] section: comm.drv=kpcomm.drv
# 3. Add to [386Enh] section: COM1FIFO=1 (for each port in use)
# 4. Restart Windows
# 3. Restart Windows
#
# Optional SYSTEM.INI [386Enh] settings (n = port number):
# COMnFIFO=0 Disable the 16550A FIFOs (default: enabled)
# COMnRxTRIGGER=x RX FIFO trigger level 1, 4, 8, or 14 bytes (default 4)
# COMnBuffer=x Minimum queue size in bytes, applied to every open
# COMnBase=x Hex base address, used when the BIOS has no entry
# COMnIRQ=x IRQ 2(=9), 3, 4, 5, 7, 9, 10, 11, 12, or 15
# COMnKeepDTR=1 CloseComm (and Windows exit) leaves DTR up (default: drop it)
#
# Key improvements over stock COMM.DRV:
# - Both RX and TX FIFOs enabled (stock: RX only)
# - RX trigger level 8 (stock: 14 -- only 2 bytes headroom)
# - RX trigger level configurable, default 4 (stock: 14 -- only 2 bytes headroom)
# - TX burst writes up to 16 bytes per THRE interrupt (stock: 1 byte)
CC = cl
@ -26,11 +34,14 @@ LINK = link
# -c Compile only
# -W3 Warning level 3
# -ASw Small model, SS!=DS (Windows DLL)
# -G2 80286 instructions (Windows 3.1 requires at least a 286)
# -Gsw No stack probes, Windows prolog/epilog
# -Ow Safe optimizations for Windows
# -Zp2 Pack structures on 2-byte boundaries (matches Windows SDK)
# -Ze Enable Microsoft extensions
CFLAGS = -c -W3 -ASw -Gsw -Ow -Zp2 -Ze
# Port I/O, CLI/STI, and far memory helpers are made intrinsic with
# #pragma intrinsic in commdrv.h.
CFLAGS = -c -W3 -ASw -G2 -Gsw -Ow -Zp2 -Ze
# Linker flags:
# /NOD No default libraries
@ -47,7 +58,7 @@ LIBS = sdllcew libw
TARGET = kpcomm.drv
# Objects
OBJS = commdrv.obj isr.obj
OBJS = commdrv.obj isr.obj lpt.obj
# -----------------------------------------------------------------------
# Build rules
@ -63,6 +74,9 @@ commdrv.obj: commdrv.c commdrv.h
isr.obj: isr.c commdrv.h
$(CC) $(CFLAGS) isr.c
lpt.obj: lpt.c commdrv.h
$(CC) $(CFLAGS) lpt.c
# -----------------------------------------------------------------------
# Clean
# -----------------------------------------------------------------------

202
drv/ring0.md Normal file
View file

@ -0,0 +1,202 @@
# Ring-0 Interrupt Handling -- Notes for Later
KPCOMM.DRV handles UART interrupts in a ring-3, protected-mode handler hooked
through DPMI. The stock Windows 3.1 COMM.DRV runs its interrupt code at
ring 0 in enhanced mode. This file records what is known about that, when
switching would be worth it, and how to do it, so the work can be picked up
later without redoing the research.
**Decision (2026-10-12): not now.** Measure first; the cheap steps below
may make it unnecessary. If it is ever needed, a small VxD is a better
design than copying the stock driver's hook.
Each statement below is marked **verified** (checked against a source) or
**recollection** (believed true, not yet checked).
## The current path and its cost
In 386 enhanced mode a UART interrupt currently goes:
1. Hardware IRQ to the VMM's interrupt table at ring 0, then to VPICD (the
virtual interrupt controller).
2. VPICD gives the IRQ to the VM that owns it (the system VM, via VCD's
virtualization of the COM IRQ) and requests a virtual interrupt there.
3. If the system VM is not running, the scheduler has to switch to it.
VPICD boosts the VM's priority, but delivery still waits for the switch
and for the VM's virtual interrupt flag to be set.
4. The DPMI host enters KPCOMM's protected-mode handler (`isrIrqNN`) at
ring 3 on its locked stack.
5. Every `in`/`out` on the UART may trap to VCD, if VCD is still trapping
the ports for the owning VM -- unknown, see step 2 of the plan. EOI on
port 20h is virtualized by VPICD.
6. IRET back down to ring 0, then resume whatever was running.
Two costs follow from this:
- **Latency.** The time from the IRQ to our handler includes VM scheduling.
It is normally small, but under load (a busy DOS box, DOS file I/O running
in V86 mode, long CLI periods elsewhere) it can approach or exceed the FIFO
budget. At 115200 baud a byte arrives every 87 us; with RX trigger 1 the
16-byte FIFO leaves about 1.3 ms of slack, with trigger 8 about 0.7 ms.
- **CPU.** Each interrupt pays several ring transitions. A full-speed
115200 stream can mean about 2,900 interrupts per second at the default RX
trigger of 4, and about 11,500 at trigger 1. A guess of 10-30 us per
reflected interrupt on a 486 would put trigger 1 at 10-35% of the CPU and
trigger 4 at roughly a quarter of that. These are recollection-level
estimates, not measurements.
Ring-0 handling would remove the VM scheduling and the ring transitions (and
any I/O trapping): the handler runs at hardware-interrupt time whichever VM
is active. It does not help when something else keeps interrupts disabled
for too long, such as floppy access in enhanced mode; only FIFO depth covers
that.
## Measure first
1. **Measure headroom (debug build).**
- Record the most bytes drained in a single `handleRx` call. That is
the FIFO fill level when the handler finally ran, which measures
latency directly.
- Also count interrupts per second and `CE_OVERRUN` occurrences.
- Log the three at close with `dbgHex16`.
- Run a 115200 download while copying files in File Manager, dragging
windows, reading a floppy, and running a DOS box in the background
(and full-screen).
- Reading it: a peak that stays well under ~12 bytes with no overruns
means ring 0 buys nothing you would notice. A peak at 14-16 means
overruns are near.
2. **Check for I/O trapping.** Time a few thousand `_inp` calls on the
UART's LSR against BIOS ticks. About 1 us each is untrapped ISA I/O;
several microseconds or more means VCD traps the port. If it traps,
acquiring the port through VCD's API (option C) may be enough on its own.
3. **Tune the trigger.** The default `COMnRxTRIGGER=4` keeps about 1 ms of
slack at a quarter of trigger 1's interrupt rate.
- **Overruns:** try 1 (about 1.3 ms of slack).
- **CPU:** try 8 (about 0.7 ms of slack, half the interrupts of 4).
The 16550A's character timeout delivers stragglers within 4 character
times at any level. If bytes below the trigger level stall until more
data arrives, the UART or emulator is not raising the timeout interrupt;
use 1 there and note which hardware it was. (An old commit message
claimed this happened at trigger 8. It was never verified.)
Measure on real hardware: emulator UART and interrupt timing is not
representative.
## Option A -- the stock driver's VCD hook
**Verified** (KB Q80901, Windows 3.1 application compatibility):
> "COMM.DRV provides its own interrupt handling in enhanced mode."
> "a new function allows COMM.DRV to register its own ring-0 handler"
The same article says `COMMDRV30=TRUE` in `[386Enh]` restores the Windows
3.0 behavior. Both COMM.DRV and VCD read `COMxBASE` and `COMxIRQ`.
**Recollection** (to be confirmed from the DDK or disassembly):
- **What changed in 3.1.** In 3.0, VCD's own ring-0 code serviced the UART
for COMM.DRV. In 3.1, COMM.DRV hands VCD the address of its own ISR, and
VCD calls it at hardware-interrupt time through ring-0 aliases of
COMM.DRV's 16-bit code and data segments. That way all comm logic stays
in COMM.DRV.
- **The protocol.** COMM.DRV reaches VCD through its protected-mode API
entry point: INT 2Fh AX=1684h with BX=000Eh (VCD's device ID) returns
ES:DI. Acquire, release, and ISR-registration functions are called
through that entry point.
- **Sources.** The Windows 3.1 DDK shipped sample sources for COMM.DRV
(`ibmcom.asm`, `ibmint.asm`, ...) and probably for VCD; the exact protocol
is there.
**How to recover the protocol:**
- From the 3.1 DDK sources, if a copy can be found.
- From the CyberCom disassembly already in hand: look for INT 2Fh with
AX=1684h and BX=000Eh, then trace what it calls through the returned
entry point. If CyberCom does it, it is the template.
- From the stock COMM.DRV: same search.
**Constraints on KPCOMM's interrupt code if it runs at ring 0:**
- **No Windows API calls.** `checkNotify` calls `PostMessage`; that must
become a deferred event (VCD/VMM schedules a callback in the system VM,
which then posts), or a flag polled from application-context calls.
- **No DPMI.** `chainIsr` (DPMI chaining) and `maskIrq` go away; VPICD owns
masking, EOI (`VPICD_Phys_EOI`), and sharing. `serviceIrq`'s
EOI/chain/spurious logic is replaced by whatever VCD expects as a return.
- **Segments and stack.** The code runs with VCD-provided ring-0 aliases.
DS must be the alias passed in, not `seg ports`. Far pointers to
GlobalAlloc buffers carry ring-3 selectors; ring 0 may load DPL-3 data
selectors, but check how VCD expects buffers to be addressed. The stack
is VCD's; 16-bit code on a 32-bit stack needs care with SP versus ESP.
- **Two paths.** Standard mode has no VxDs, so the current DPMI path must
remain as a fallback, and both must share `servicePort`/`handleRx`/
`handleTx` without duplicating them.
- **Undocumented.** The interface is undocumented for third-party drivers
and specific to Windows 3.1 (and possibly differs in WfW 3.11).
The parts of `isr.c` that are already ring-safe are `servicePort`,
`handleRx`, `handleTx`, `handleLsr`, `handleMsr`, `updateRxFlow`,
`releaseRxFlow`, `updateTxHold`, `startTx`, `writeIer`, and `writeMcr`.
They use only port I/O and the port state. `raiseEvents` is safe;
`checkNotify` is not.
## Option B -- a KPCOMM VxD
The cleaner design: a small 32-bit virtual device that owns the UART at
ring 0, as Windows for Workgroups 3.11's SERIAL.386 does for its COMM.DRV
(recollection: WfW 3.11 shipped SERIAL.386, which takes over from VCD's comm
handling).
- **Interrupts.** `VPICD_Virtualize_IRQ` for the port's IRQ. The VxD's
hardware-interrupt callback services the UART and fills ring buffers in
locked memory, then calls `VPICD_Phys_EOI`.
- **Talking to COMM.DRV.** COMM.DRV gets a PM API entry point (INT 2Fh
1684h with the VxD's ID) to open and close ports, and to map the shared
buffers and ComDEB-like state into its address space. The ring-3
COMM.DRV keeps the Windows API and DCB logic, and the buffers stay
single-producer/single-consumer as now.
- **Notifications.** `Call_Priority_VM_Event` or a VMM event into the
system VM, then `PostMessage` from there.
- **Port ownership.** Either cooperate with VCD (acquire the port through
VCD so DOS boxes get the contention dialog) or replace VCD entirely with
`device=` in `[386Enh]`, as TurboCom-style products did (recollection).
Replacing VCD means taking on DOS-box virtualization too.
- **Toolchain.** Windows 3.1 DDK (VxD includes, `vmm.inc`/`vpicd.inc`), MASM
5.10b or 6.x, LINK386, and ADDHDR or equivalent. Debug with WDEB386 on a
serial terminal. MSVC 1.52 cannot build VxDs.
- **Cost.** The largest of the options. A bug hangs the machine, and
standard mode still needs the DPMI path.
## Option C -- VCD port acquisition only
If step 2 of the plan shows VCD trapping UART I/O for the system VM,
acquiring the port through VCD's PM API may stop the trapping while keeping
the ring-3 handler. It is cheaper than A or B and addresses only the
per-I/O cost, not scheduling latency. It needs the same VCD API
information as option A (recollection-level today).
## Summary
| Option | Fixes latency | Fixes CPU cost | Effort | Risk |
|---|---|---|---|---|
| Tune trigger level | No | Yes, up to 4-8x fewer interrupts | Trivial | Less slack per interrupt |
| C: VCD acquisition | No | Partly, only if I/O is trapped | Small-medium | Undocumented API |
| A: VCD ring-0 hook | Yes | Yes | Medium-large | Undocumented; restructured ISR; two paths |
| B: KPCOMM VxD | Yes | Yes | Large | New toolchain; ring-0 debugging; two paths |
## References
- 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
- Windows 3.1 DDAG, communication drivers:
https://dos-help.soulsphere.org/ddag31qh.hlp/Communication-Driver_Functions.html
- `README.md` in this directory, for the current architecture