Basically a complete rewrite.
This commit is contained in:
parent
0e8bb9f989
commit
9265a76b65
9 changed files with 3635 additions and 1988 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
631
drv/README.md
Normal 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
|
||||
2700
drv/commdrv.c
2700
drv/commdrv.c
File diff suppressed because it is too large
Load diff
632
drv/commdrv.h
632
drv/commdrv.h
|
|
@ -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
|
||||
|
|
|
|||
217
drv/lpt.c
Normal file
217
drv/lpt.c
Normal 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;
|
||||
}
|
||||
26
drv/makefile
26
drv/makefile
|
|
@ -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
202
drv/ring0.md
Normal 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
|
||||
Loading…
Add table
Reference in a new issue