diff --git a/ansibbs/KPCOMM.PAS b/ansibbs/KPCOMM.PAS index f02821c..f4ce88c 100644 --- a/ansibbs/KPCOMM.PAS +++ b/ansibbs/KPCOMM.PAS @@ -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; diff --git a/ansibbs/KPCOMM.md b/ansibbs/KPCOMM.md index 0e796a1..7d81a7a 100644 --- a/ansibbs/KPCOMM.md +++ b/ansibbs/KPCOMM.md @@ -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. diff --git a/drv/README.md b/drv/README.md new file mode 100644 index 0000000..f11b8f2 --- /dev/null +++ b/drv/README.md @@ -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 diff --git a/drv/commdrv.c b/drv/commdrv.c index 862efc7..35f471c 100644 --- a/drv/commdrv.c +++ b/drv/commdrv.c @@ -1,331 +1,284 @@ // commdrv.c - KPCOMM.DRV -- High-speed COMM.DRV replacement // -// All exported COMM.DRV functions, ring buffer management, port -// initialization, 16550 FIFO detection, and flow control. +// Exported COMM.DRV entry points (Windows 3.1 DDAG signatures), port +// configuration, ring buffer management, and DLL entry/exit. // -// Drop-in replacement for Windows 3.1 stock COMM.DRV with proper +// Drop-in replacement for the Windows 3.1 COMM.DRV with proper 16550 // FIFO management for reliable operation at 57600 and 115200 baud. #include "commdrv.h" -#include -#include +#include // ----------------------------------------------------------------------- -// Debug logging via OutputDebugString +// SYSTEM.INI [386Enh] keys, named COMn // ----------------------------------------------------------------------- -#define DBG_ENABLED 0 +#define INI_FILE "SYSTEM.INI" +#define INI_SECTION "386Enh" +#define INI_KEY_BASE "Base" // COMnBase: hex I/O address, used only if the BIOS has none +#define INI_KEY_BUFFER "Buffer" // COMnBuffer: minimum queue size +#define INI_KEY_FIFO "FIFO" // COMnFIFO: Boolean, 16550A FIFOs enabled by default +#define INI_KEY_IRQ "IRQ" // COMnIRQ: interrupt line +#define INI_KEY_KEEPDTR "KeepDTR" // COMnKeepDTR: Boolean, CloseComm leaves DTR up (default off) +#define INI_KEY_TRIGGER "RxTRIGGER" // COMnRxTRIGGER: RX FIFO trigger level in bytes (1/4/8/14) +#define INI_KEY_MAX 16 // "COMn" + longest suffix + NUL +#define INI_VALUE_MAX 16 +#define COM_KEY_PREFIX "COM1" +#define COM_KEY_DIGIT 3 // Index of the port digit in COM_KEY_PREFIX +#define DEFAULT_TRIGGER 4 // COMnRxTRIGGER default (bytes): ~1 ms slack at 115200, 1/4 the interrupts of 1 +#define HEX_PREFIX_LEN 2 // "0x" +#define HEX_DIGIT_BITS 4 + +// trmcom gives up when the transmitter makes no progress for this many BIOS ticks (~5 s) +#define DRAIN_TIMEOUT_TICKS 91 + +// Modem lines a DCB timeout can make output wait for (see waitTxLines) +#define TX_LINE_COUNT 3 + +typedef struct { + uint16_t timeoutOffset; // offsetof(DCB, Timeout) + uint8_t msrBit; // Line state in the MSR + uint16_t error; // CE_* bit reported when the wait times out +} TxLineT; #if DBG_ENABLED -static void dbgStr(const char FAR *msg) { - OutputDebugString(msg); -} - -// Log a label and a 16-bit hex value: "label: 0xNNNN\r\n" -static void dbgHex16(const char FAR *label, uint16_t val) { - static const char hex[] = "0123456789ABCDEF"; - char buf[48]; - int i; - - for (i = 0; i < 40 && label[i]; i++) { - buf[i] = label[i]; - } - buf[i++] = ':'; - buf[i++] = ' '; - buf[i++] = '0'; - buf[i++] = 'x'; - buf[i++] = hex[(val >> 12) & 0xF]; - buf[i++] = hex[(val >> 8) & 0xF]; - buf[i++] = hex[(val >> 4) & 0xF]; - buf[i++] = hex[val & 0xF]; - buf[i++] = '\r'; - buf[i++] = '\n'; - buf[i] = '\0'; - OutputDebugString(buf); -} - -// Log a label and a signed 16-bit value: "label: -NNNN\r\n" -static void dbgInt16(const char FAR *label, int16_t val) { - char buf[48]; - int i; - uint16_t uv; - - for (i = 0; i < 40 && label[i]; i++) { - buf[i] = label[i]; - } - buf[i++] = ':'; - buf[i++] = ' '; - - if (val < 0) { - buf[i++] = '-'; - uv = (uint16_t)(-(int)val); - } else { - uv = (uint16_t)val; - } - - // Print decimal (up to 5 digits) - { - char digits[6]; - int n = 0; - do { - digits[n++] = (char)('0' + (uv % 10)); - uv /= 10; - } while (uv > 0); - while (n > 0) { - buf[i++] = digits[--n]; - } - } - - buf[i++] = '\r'; - buf[i++] = '\n'; - buf[i] = '\0'; - OutputDebugString(buf); -} - -// Log a FAR pointer as "label: SSSS:OOOO\r\n" -static void dbgFarPtr(const char FAR *label, void FAR *ptr) { - static const char hex[] = "0123456789ABCDEF"; - char buf[48]; - int i; - uint16_t seg; - uint16_t off; - - seg = _FP_SEG(ptr); - off = _FP_OFF(ptr); - - for (i = 0; i < 32 && label[i]; i++) { - buf[i] = label[i]; - } - buf[i++] = ':'; - buf[i++] = ' '; - buf[i++] = hex[(seg >> 12) & 0xF]; - buf[i++] = hex[(seg >> 8) & 0xF]; - buf[i++] = hex[(seg >> 4) & 0xF]; - buf[i++] = hex[seg & 0xF]; - buf[i++] = ':'; - buf[i++] = hex[(off >> 12) & 0xF]; - buf[i++] = hex[(off >> 8) & 0xF]; - buf[i++] = hex[(off >> 4) & 0xF]; - buf[i++] = hex[off & 0xF]; - buf[i++] = '\r'; - buf[i++] = '\n'; - buf[i] = '\0'; - OutputDebugString(buf); -} - -// Hex dump first 'count' bytes at a FAR pointer -static void dbgDump(const char FAR *label, void FAR *ptr, int count) { - static const char hex[] = "0123456789ABCDEF"; - char buf[80]; - uint8_t FAR *p; - int i; - int bi; - - p = (uint8_t FAR *)ptr; - - // Print label - for (bi = 0; bi < 32 && label[bi]; bi++) { - buf[bi] = label[bi]; - } - buf[bi++] = ':'; - buf[bi++] = ' '; - - for (i = 0; i < count && bi < 70; i++) { - buf[bi++] = hex[(p[i] >> 4) & 0xF]; - buf[bi++] = hex[p[i] & 0xF]; - buf[bi++] = ' '; - } - buf[bi++] = '\r'; - buf[bi++] = '\n'; - buf[bi] = '\0'; - OutputDebugString(buf); -} -#else -#define dbgStr(msg) ((void)0) -#define dbgHex16(label, val) ((void)0) -#define dbgInt16(label, val) ((void)0) -#define dbgFarPtr(label, p) ((void)0) -#define dbgDump(l, p, n) ((void)0) +#define DBG_LABEL_MAX 32 // Longest label dbgHex16 copies +#define DBG_LINE_MAX 48 // Label + ": 0x" + 4 hex digits + CRLF + NUL #endif // ----------------------------------------------------------------------- -// Prototypes +// Prototypes -- exported entry points // ----------------------------------------------------------------------- -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); +int16_t FAR PASCAL _export cclrbrk(int16_t commId); +uint16_t FAR * FAR PASCAL _export cevt(int16_t commId, int16_t evtMask); +uint16_t FAR PASCAL _export cevtget(int16_t commId, int16_t evtMask); +int32_t FAR PASCAL _export cextfcn(int16_t commId, int16_t func); +int16_t FAR PASCAL _export cflush(int16_t commId, int16_t queue); +int FAR PASCAL _export commNotifyWndProc(void); +int16_t FAR PASCAL _export commWriteString(int16_t commId, void FAR *buf, int16_t len); +int16_t FAR PASCAL _export csetbrk(int16_t commId); +uint16_t FAR PASCAL _export ctx(int16_t commId, int16_t ch); +int16_t FAR PASCAL _export enableNotification(int16_t commId, HWND hwnd, int16_t rxTrigger, int16_t txTrigger); +DCB FAR * FAR PASCAL _export getdcb(int16_t commId); +int16_t FAR PASCAL _export inicom(DCB FAR *dcb); +void FAR PASCAL _export reactivateOpenCommPorts(void); +int16_t FAR PASCAL _export readCommString(int16_t commId, void FAR *buf, int16_t len); +int16_t FAR PASCAL _export reccom(int16_t commId); +int16_t FAR PASCAL _export setcom(DCB FAR *dcb); +int16_t FAR PASCAL _export setque(int16_t commId, QdbT FAR *qdb); +uint16_t FAR PASCAL _export sndcom(int16_t commId, int16_t ch); +uint16_t FAR PASCAL _export stacom(int16_t commId, COMSTAT FAR *stat); +void FAR PASCAL _export suspendOpenCommPorts(void); +int16_t FAR PASCAL _export trmcom(int16_t commId); -int FAR PASCAL _export commNotifyWndProc(void); -int FAR PASCAL LibMain(HANDLE hInstance, WORD wDataSeg, WORD wHeapSize, LPSTR lpCmdLine); -int FAR PASCAL _export WEP(int nParam); - -void applyBaudRate(PortStateT *port, uint16_t baud); -void applyLineParams(PortStateT *port, uint8_t byteSize, uint8_t parity, uint8_t stopBits); -static uint32_t cbrToBaud(uint16_t cbr); -int16_t detect16550(uint16_t baseAddr); -void enableFifo(PortStateT *port); -static void freeBuffers(PortStateT *port); -static int16_t initBuffers(PortStateT *port, uint16_t rxSz, uint16_t txSz); -static void initPortState(PortStateT *port, int16_t commId); -void primeTx(PortStateT *port); -static void readPortConfig(int16_t commId, uint16_t FAR *baseAddr, uint8_t FAR *irq); -static uint16_t readSystemIni(const char FAR *section, const char FAR *key, uint16_t defVal); +int FAR PASCAL LibMain(HANDLE hInstance, WORD wDataSeg, WORD wHeapSize, LPSTR lpCmdLine); +int FAR PASCAL _export WEP(int nParam); // ----------------------------------------------------------------------- -// Port base addresses and IRQ defaults (overridden by SYSTEM.INI) +// Prototypes -- internal helpers (shared ones are declared in commdrv.h) // ----------------------------------------------------------------------- -static const uint16_t portBase[MAX_PORTS] = { +static void addErrors(PortStateT *port, uint16_t errors); +static bool allocBuffer(BufferT FAR *buf, uint16_t size); +static int16_t applyDcb(PortStateT *port, const DCB FAR *dcb, bool opening); +static uint16_t baudDivisor(uint16_t baud); +static uint16_t biosDataSelector(void); +static uint32_t cbrToBaud(uint16_t cbr); +static void clearUartStatus(uint16_t base); +static void closePort(PortStateT *port); +static int32_t comBaseIrq(int16_t commId); +static void comIniKey(char FAR *key, int16_t commId, const char FAR *suffix); +static int16_t dcbToLine(const DCB FAR *dcb, uint16_t FAR *divisor, uint8_t FAR *lcr); +static bool detect16550(uint16_t base); +static bool drainTx(PortStateT *port); +static void enableFifo(PortStateT *port); +static void freeBuffer(BufferT FAR *buf); +static PortStateT *getOpenPort(int16_t commId); +static int16_t hexDigit(char ch); +static void initPortState(PortStateT *port, int16_t commId, uint16_t base, uint8_t irq); +static uint16_t queueSize(int16_t commId, uint16_t requested); +static uint16_t readComIni(int16_t commId, const char FAR *suffix, uint16_t defVal); +static bool readComIniBool(int16_t commId, const char FAR *suffix, bool defVal); +static uint16_t readComIniHex(int16_t commId, const char FAR *suffix, uint16_t defVal); +static void readComIniString(int16_t commId, const char FAR *suffix, char FAR *value); +static void readPortConfig(int16_t commId, uint16_t FAR *base, uint8_t FAR *irq); +static uint16_t readRx(PortStateT *port, uint8_t FAR *dst, uint16_t len); +static void resetRx(PortStateT *port); +static void resetTx(PortStateT *port); +static bool resolvePostMessage(void); +static uint8_t rxTriggerBits(int16_t commId); +static int16_t setBreak(int16_t commId, bool on); +static int16_t triggerCode(uint16_t bytes); +static bool uartPresent(uint16_t base); +static uint16_t waitTxLines(PortStateT *port); +static void writeLine(PortStateT *port, uint16_t divisor, uint8_t lcr); +static uint16_t writeTx(PortStateT *port, const uint8_t FAR *src, uint16_t len); + +// ----------------------------------------------------------------------- +// Default port resources, used when neither the BIOS nor SYSTEM.INI says +// ----------------------------------------------------------------------- +static const uint16_t defaultBase[MAX_PORTS] = { COM1_BASE, COM2_BASE, COM3_BASE, COM4_BASE }; -static const uint8_t portIrq[MAX_PORTS] = { +static const uint8_t defaultIrq[MAX_PORTS] = { COM1_IRQ, COM2_IRQ, COM3_IRQ, COM4_IRQ }; -// ----------------------------------------------------------------------- -// Global port state array -- one PortStateT per COM port. -// Accessed from both application context and ISR context. -// Segment address loaded into DS by ISR entry points (isr3/isr4). -// ----------------------------------------------------------------------- -PortStateT ports[MAX_PORTS]; +// RX FIFO trigger level in bytes, indexed by the FCR trigger code +static const uint8_t fifoTriggerBytes[] = { 1, 4, 8, 14 }; + +static const TxLineT txLines[TX_LINE_COUNT] = { + { offsetof(DCB, CtsTimeout), MSR_CTS, CE_CTSTO }, + { offsetof(DCB, DsrTimeout), MSR_DSR, CE_DSRTO }, + { offsetof(DCB, RlsTimeout), MSR_DCD, CE_RLSDTO } +}; // ----------------------------------------------------------------------- -// DLL instance handle (saved in LibMain for resource loading) +// Global state // ----------------------------------------------------------------------- -static HANDLE ghInstance = NULL; - -// ISR hit counter for diagnostics (incremented in isr4, wraps at 65535) -volatile uint16_t isrHitCount = 0; +PortStateT ports[MAX_PORTS]; // One per COM port, indexed by cid; shared with the ISR +volatile bool portsSuspended = false; // Set while a DOS application owns the hardware +static uint16_t biosSelector = 0; // DPMI selector for segment 0040h (0 = not created yet) // ----------------------------------------------------------------------- -// applyBaudRate - Set the baud rate divisor on the UART +// addErrors - Record CE_* bits from application context // -// The DCB BaudRate field is a 16-bit UINT. For rates that fit in 16 bits -// (up to 57600), the raw value is used directly. For higher rates like -// 115200, CBR_* index constants (0xFF00+) encode the rate. We detect -// these and translate via cbrToBaud(). +// The ISR also updates errorFlags, so the read-modify-write is guarded. // ----------------------------------------------------------------------- -void applyBaudRate(PortStateT *port, uint16_t baud) { - uint32_t actualBaud; +static void addErrors(PortStateT *port, uint16_t errors) { + _disable(); + port->errorFlags |= errors; + _enable(); +} + + +// ----------------------------------------------------------------------- +// allocBuffer - Allocate a fixed, page-locked ring buffer +// +// GMEM_FIXED keeps the address stable and GlobalPageLock keeps it +// resident in enhanced mode, so the ISR can never fault on it. +// GMEM_SHARE makes the driver own it rather than the calling task, so an +// application that dies without CloseComm cannot free memory the ISR is +// still filling. Never call with interrupts disabled: the global heap may +// need compaction or DPMI. +// ----------------------------------------------------------------------- +static bool allocBuffer(BufferT FAR *buf, uint16_t size) { + HGLOBAL handle; + + handle = GlobalAlloc(GMEM_FIXED | GMEM_SHARE, (DWORD)size); + if (!handle) { + return false; + } + GlobalPageLock(handle); + + buf->handle = handle; + buf->data = (uint8_t FAR *)GlobalLock(handle); + return true; +} + + +// ----------------------------------------------------------------------- +// applyDcb - Validate a DCB and make it the port's live configuration +// +// Shared by inicom and setcom. Throttling done under the old settings is +// released first, then line settings, DTR/RTS, transmit holds, and receive +// throttling are re-derived from the new DCB. fDtrDisable/fRtsDisable +// drive the lines at open and when they change, so an unrelated SetCommState +// does not undo an EscapeCommFunction. Returns 0 or an IE_* code. +// ----------------------------------------------------------------------- +static int16_t applyDcb(PortStateT *port, const DCB FAR *dcb, bool opening) { uint16_t divisor; uint8_t lcr; - uint16_t base; + int16_t err; + bool dtrChanged; + bool rtsChanged; - if (baud == 0) { - return; + err = dcbToLine(dcb, &divisor, &lcr); + if (err != 0) { + return err; } - // Translate CBR_* index constants to actual baud rate - if ((baud & 0xFF00) == 0xFF00) { - actualBaud = cbrToBaud(baud); - } else { - actualBaud = (uint32_t)baud; + dtrChanged = opening || dcb->fDtrDisable != port->dcb.fDtrDisable; + rtsChanged = opening || dcb->fRtsDisable != port->dcb.fRtsDisable; + + _disable(); + releaseRxFlow(port); + _fmemcpy(&port->dcb, dcb, sizeof(DCB)); + if (dtrChanged) { + port->dtrOn = !dcb->fDtrDisable; } - - if (actualBaud == 0) { - return; + if (rtsChanged) { + port->rtsOn = !dcb->fRtsDisable; } - - base = port->baseAddr; - divisor = (uint16_t)(BAUD_DIVISOR_BASE / actualBaud); - - // Guard: divisor 0 means the UART treats it as 65536, giving ~1.76 baud. - // This can happen when BuildCommDCB stores a raw truncated value for - // 115200 (e.g. 0xE101 = 57601) and a future rate exceeds 115200. - if (divisor == 0) { - divisor = 1; + writeLine(port, divisor, lcr); + port->comDeb.baudRate = dcb->BaudRate; + if (!dcb->fOutX) { + port->txHold = (uint8_t)(port->txHold & ~TX_HOLD_XOFF); } + writeMcr(port); + updateTxHold(port); + updateRxFlow(port); + _enable(); - // Set DLAB to access divisor latch - lcr = (uint8_t)_inp(base + UART_LCR); - _outp(base + UART_LCR, lcr | LCR_DLAB); - - _outp(base + UART_DLL, (uint8_t)(divisor & 0xFF)); - _outp(base + UART_DLM, (uint8_t)(divisor >> 8)); - - // Clear DLAB - _outp(base + UART_LCR, lcr); - - port->baudRate = baud; + return 0; } // ----------------------------------------------------------------------- -// applyLineParams - Set word length, parity, and stop bits on UART -// ----------------------------------------------------------------------- -void applyLineParams(PortStateT *port, uint8_t byteSize, uint8_t parity, uint8_t stopBits) { - uint8_t lcr; - uint16_t base; - - base = port->baseAddr; - lcr = 0; - - // Word length - if (byteSize >= 5 && byteSize <= 8) { - lcr |= (byteSize - 5); - } else { - lcr |= LCR_WLS_8; - } - - // Stop bits - if (stopBits == 2) { - lcr |= LCR_STB; - } - - // Parity - switch (parity) { - case ODDPARITY: - lcr |= LCR_PEN; - break; - case EVENPARITY: - lcr |= LCR_PEN | LCR_EPS; - break; - case MARKPARITY: - lcr |= LCR_PEN | LCR_SPAR; - break; - case SPACEPARITY: - lcr |= LCR_PEN | LCR_EPS | LCR_SPAR; - break; - case NOPARITY: - default: - break; - } - - dbgHex16("KPCOMM: applyLine byteSize", (uint16_t)byteSize); - dbgHex16("KPCOMM: applyLine parity", (uint16_t)parity); - dbgHex16("KPCOMM: applyLine stopBits", (uint16_t)stopBits); - dbgHex16("KPCOMM: applyLine LCR", (uint16_t)lcr); - - _outp(base + UART_LCR, lcr); - - port->byteSize = byteSize; - port->parity = parity; - port->stopBits = stopBits; -} - - -// ----------------------------------------------------------------------- -// cbrToBaud - Translate CBR_* index constant to actual baud rate +// baudDivisor - Convert a DCB BaudRate value to a UART divisor // -// Returns baud rate as uint32_t (needed for 115200 which exceeds 16 bits). -// Returns 0 for unrecognized constants. +// A high byte of 0xFF selects a CBR_* index; anything else is a raw rate +// (DDAG). Returns 0 for unsupported rates. The division truncates on +// purpose: raw values 57601-65279 give divisor 1 (115200 baud), which is +// how a 16-bit BaudRate can ask for 115200 (BuildCommDCB has been seen to +// store 0xE101 = 57601 for it). +// ----------------------------------------------------------------------- +static uint16_t baudDivisor(uint16_t baud) { + uint32_t rate; + + if ((baud & CBR_INDEX_MASK) == CBR_INDEX_MASK) { + rate = cbrToBaud(baud); + } else { + rate = baud; + } + + if (rate < MIN_RAW_BAUD) { + return 0; + } + return (uint16_t)(UART_CLOCK_DIV / rate); +} + + +// ----------------------------------------------------------------------- +// biosDataSelector - Selector for the BIOS data area (segment 0040h) +// +// Cached after the first DPMI call. Returns 0 if DPMI refuses. +// ----------------------------------------------------------------------- +static uint16_t biosDataSelector(void) { + uint16_t sel; + int16_t failed; + + if (biosSelector == 0) { + _asm { + mov ax, DPMI_SEG_TO_DESC + mov bx, BIOS_DATA_SEG + int DPMI_INT + mov sel, ax + sbb ax, ax + mov failed, ax + } + if (!failed) { + biosSelector = sel; + } + } + return biosSelector; +} + + +// ----------------------------------------------------------------------- +// cbrToBaud - Translate a CBR_* index to a rate (0 = unsupported) +// +// CBR_128000 and CBR_256000 are beyond a 1.8432 MHz UART and are rejected. // ----------------------------------------------------------------------- static uint32_t cbrToBaud(uint16_t cbr) { switch (cbr) { @@ -351,7 +304,7 @@ static uint32_t cbrToBaud(uint16_t cbr) { return 38400UL; case CBR_56000: return 56000UL; - case CBR_115200: + case KPCBR_115200: return 115200UL; default: return 0UL; @@ -360,102 +313,62 @@ static uint32_t cbrToBaud(uint16_t cbr) { // ----------------------------------------------------------------------- -// cclrbrk - Clear break signal (ordinal 14) +// cclrbrk - Clear break (ordinal 14) // ----------------------------------------------------------------------- int16_t FAR PASCAL _export cclrbrk(int16_t commId) { - PortStateT *port; - uint8_t lcr; - - dbgInt16("KPCOMM: cclrbrk Id", commId); - - if (commId < 0 || commId >= MAX_PORTS) { - return -1; - } - - port = &ports[commId]; - if (!port->isOpen) { - return -1; - } - - lcr = (uint8_t)_inp(port->baseAddr + UART_LCR); - _outp(port->baseAddr + UART_LCR, lcr & ~LCR_SBRK); - - port->breakState = FALSE; - return 0; + return setBreak(commId, false); } // ----------------------------------------------------------------------- -// cevt - Set event mask and return pointer to event word (ordinal 11) +// cevt - Set the event mask; return a pointer to the event word (ordinal 11) // -// Returns a FAR pointer to the event word (packed into int32_t). -// This matches the stock COMM.DRV behavior -- the event word is a -// volatile uint16_t that accumulates event bits until cleared. +// Only events enabled here are recorded in the event word (DDAG). // ----------------------------------------------------------------------- -int32_t FAR PASCAL _export cevt(int16_t commId, int16_t evtMask) { +uint16_t FAR * FAR PASCAL _export cevt(int16_t commId, int16_t evtMask) { PortStateT *port; - uint16_t FAR *ptr; - int32_t retVal; + LptStateT *lpt; -#if DBG_ENABLED - { - uint16_t ssSeg; - uint16_t bpVal; - _asm mov ssSeg, ss - _asm mov bpVal, bp - dbgHex16("KPCOMM: cevt SS", ssSeg); - dbgHex16("KPCOMM: cevt BP", bpVal); - dbgDump("KPCOMM: cevt stk", (void FAR *)((uint32_t)ssSeg << 16 | (bpVal + 6)), 16); - } -#endif - - dbgInt16("KPCOMM: cevt Id", commId); - dbgHex16("KPCOMM: cevt mask", (uint16_t)evtMask); - - if (commId < 0 || commId >= MAX_PORTS) { - return 0L; + lpt = getOpenLpt(commId); + if (lpt) { + lpt->evtMask = (uint16_t)evtMask; + return (uint16_t FAR *)&lpt->evtWord; } - port = &ports[commId]; - if (!port->isOpen) { - dbgStr("KPCOMM: cevt not open\r\n"); - return 0L; + port = getOpenPort(commId); + if (!port) { + return NULL; } port->comDeb.evtMask = (uint16_t)evtMask; - - ptr = &port->comDeb.evtWord; - retVal = (int32_t)(void FAR *)ptr; - dbgHex16("KPCOMM: cevt retSeg", (uint16_t)(retVal >> 16)); - dbgHex16("KPCOMM: cevt retOff", (uint16_t)(retVal & 0xFFFF)); - dbgStr("KPCOMM: cevt OK\r\n"); - return retVal; + return (uint16_t FAR *)&port->comDeb.evtWord; } // ----------------------------------------------------------------------- -// cevtget - Read and clear event word (ordinal 12) -// -// Returns current event bits masked by evtMask, then clears those bits. +// cevtget - Return and clear the requested event bits (ordinal 12) // ----------------------------------------------------------------------- uint16_t FAR PASCAL _export cevtget(int16_t commId, int16_t evtMask) { PortStateT *port; + LptStateT *lpt; uint16_t events; - dbgInt16("KPCOMM: cevtget Id", commId); - - if (commId < 0 || commId >= MAX_PORTS) { - return 0; + lpt = getOpenLpt(commId); + if (lpt) { + events = lpt->evtWord & (uint16_t)evtMask; + lpt->evtWord &= ~(uint16_t)evtMask; + return events; } - port = &ports[commId]; - if (!port->isOpen) { + port = getOpenPort(commId); + if (!port) { return 0; } _disable(); - events = port->comDeb.evtWord & (uint16_t)evtMask; + events = port->comDeb.evtWord & (uint16_t)evtMask; port->comDeb.evtWord &= ~(uint16_t)evtMask; + refreshNotify(port, CN_EVENT); _enable(); return events; @@ -463,116 +376,115 @@ uint16_t FAR PASCAL _export cevtget(int16_t commId, int16_t evtMask) { // ----------------------------------------------------------------------- -// cextfcn - Escape function for DTR/RTS/break control (ordinal 9) +// cextfcn - Extended functions / EscapeCommFunction (ordinal 9) // -// Note: 16-bit API returns 0 on success (unlike Win32 nonzero=success). +// Returns 0 on success (16-bit API), a negative value for an unknown +// function or port, or the requested value for the GET* queries. Also +// accepts the KPCOMM extensions KPESC_KEEP_DTR_ON_CLOSE and +// KPESC_DROP_DTR_ON_CLOSE (see commdrv.h). // ----------------------------------------------------------------------- -int16_t FAR PASCAL _export cextfcn(int16_t commId, int16_t func) { +int32_t FAR PASCAL _export cextfcn(int16_t commId, int16_t func) { PortStateT *port; - uint8_t mcr; - uint16_t base; - - dbgInt16("KPCOMM: cextfcn Id", commId); - dbgInt16("KPCOMM: cextfcn func", func); - - if (commId < 0 || commId >= MAX_PORTS) { - return -1; - } - - port = &ports[commId]; - if (!port->isOpen) { - return -1; - } - - base = port->baseAddr; - mcr = (uint8_t)_inp(base + UART_MCR); + LptStateT *lpt; + int32_t result; + // Queries that do not need an open port switch (func) { - case SETDTR: - mcr |= MCR_DTR; - port->dtrState = TRUE; - break; - case CLRDTR: - mcr &= ~MCR_DTR; - port->dtrState = FALSE; - break; - case SETRTS: - mcr |= MCR_RTS; - port->rtsState = TRUE; - break; - case CLRRTS: - mcr &= ~MCR_RTS; - port->rtsState = FALSE; - break; - case ESC_SETBREAK: - return csetbrk(commId); - case ESC_CLRBREAK: - return cclrbrk(commId); - case RESETDEV: - // No-op for serial ports + case GETMAXCOM: + return MAX_PORTS - 1; + case GETMAXLPT: + return lptMaxId(); + case GETBASEIRQ: + return isLptId(commId) ? lptBaseIrq(commId) : comBaseIrq(commId); + } + + lpt = getOpenLpt(commId); + if (lpt) { + if (func == RESETDEV) { + lptReset(lpt); + return 0; + } + return DRV_ERROR; + } + + port = getOpenPort(commId); + if (!port) { + return DRV_ERROR; + } + + result = 0; + _disable(); + switch (func) { + case SETXOFF: + port->txHold = (uint8_t)(port->txHold | TX_HOLD_XOFF); break; case SETXON: - // Resume TX as if XON received - _disable(); - port->txStopped = FALSE; - if (port->txCount > 0) { - _outp(base + UART_IER, (uint8_t)(_inp(base + UART_IER) | IER_THRE)); - } - _enable(); - return 0; - case SETXOFF: - // Stop TX as if XOFF received - port->txStopped = TRUE; - return 0; + port->txHold = (uint8_t)(port->txHold & ~TX_HOLD_XOFF); + startTx(port); + break; + case SETRTS: + port->rtsOn = true; + writeMcr(port); + break; + case CLRRTS: + port->rtsOn = false; + writeMcr(port); + break; + case SETDTR: + port->dtrOn = true; + writeMcr(port); + break; + case CLRDTR: + port->dtrOn = false; + writeMcr(port); + break; + case RESETDEV: + // Printer reset; nothing to do for a serial port + break; + case KPESC_KEEP_DTR_ON_CLOSE: + port->keepDtrOnClose = true; + break; + case KPESC_DROP_DTR_ON_CLOSE: + port->keepDtrOnClose = false; + break; default: - return -1; + result = DRV_ERROR; + break; } + _enable(); - _outp(base + UART_MCR, mcr); - return 0; + return result; } // ----------------------------------------------------------------------- -// cflush - Flush receive or transmit buffer (ordinal 10) +// cflush - Discard the transmit (0) or receive (1) queue (ordinal 10) // ----------------------------------------------------------------------- int16_t FAR PASCAL _export cflush(int16_t commId, int16_t queue) { PortStateT *port; - dbgInt16("KPCOMM: cflush Id", commId); - - if (commId < 0 || commId >= MAX_PORTS) { - return -1; + if (getOpenLpt(commId)) { + return 0; } - port = &ports[commId]; - if (!port->isOpen) { - return -1; + port = getOpenPort(commId); + if (!port || (queue != FLUSH_TX && queue != FLUSH_RX)) { + return DRV_ERROR; } _disable(); if (queue == FLUSH_RX) { - port->rxHead = 0; - port->rxTail = 0; - port->rxCount = 0; - port->comDeb.qInHead = 0; - port->comDeb.qInTail = 0; - port->comDeb.qInCount = 0; - // Reset FIFO to clear hardware buffer too - if (port->is16550 && port->fifoEnabled) { - _outp(port->baseAddr + UART_FCR, FCR_ENABLE | FCR_RX_RESET | port->fifoTrigger); + resetRx(port); + if (port->useFifo) { + _outp(port->comDeb.baseAddr + UART_FCR, FCR_ENABLE | FCR_RX_RESET | port->comDeb.fifoTrigger); } } else { - port->txHead = 0; - port->txTail = 0; - port->txCount = 0; - port->comDeb.qOutHead = 0; - port->comDeb.qOutTail = 0; - port->comDeb.qOutCount = 0; - if (port->is16550 && port->fifoEnabled) { - _outp(port->baseAddr + UART_FCR, FCR_ENABLE | FCR_TX_RESET | port->fifoTrigger); + resetTx(port); + if (port->useFifo) { + _outp(port->comDeb.baseAddr + UART_FCR, FCR_ENABLE | FCR_TX_RESET | port->comDeb.fifoTrigger); } } + refreshNotify(port, (uint8_t)((queue == FLUSH_RX) ? CN_RECEIVE : 0)); _enable(); return 0; @@ -580,1115 +492,1355 @@ int16_t FAR PASCAL _export cflush(int16_t commId, int16_t queue) { // ----------------------------------------------------------------------- -// commWriteString - Write data (ordinal 19, alias for sndcom) +// clearUartStatus - Read every UART status source so nothing is pending // ----------------------------------------------------------------------- -int16_t FAR PASCAL _export commWriteString(int16_t commId, void FAR *buf, int16_t len) { - dbgInt16("KPCOMM: commWriteStr Id", commId); - return sndcom(commId, buf, len); +static void clearUartStatus(uint16_t base) { + (void)_inp(base + UART_LSR); + (void)_inp(base + UART_MSR); + (void)_inp(base + UART_RBR); + (void)_inp(base + UART_IIR); } // ----------------------------------------------------------------------- -// commNotifyWndProc - Ordinal 101 stub (undocumented, Win 3.1 compat) +// closePort - Release a COM port's hardware, IRQ, and buffers // -// The stock COMM.DRV and CyberCom export an unnamed function at ordinal -// 101. Purpose is undocumented; may be an internal callback used by -// USER.EXE. CyberCom's implementation uses plain RETF (no params). -// Return 0 (not handled). +// Does not wait for output; trmcom drains first. Drops RTS and OUT2, and +// DTR as well unless keepDtrOnClose asks for the connection to survive +// (then a modem stays online for the next program, and with RTS/CTS flow +// control it holds incoming data until RTS is raised again). +// ----------------------------------------------------------------------- +static void closePort(PortStateT *port) { + uint16_t base; + + base = port->comDeb.baseAddr; + + _disable(); + port->isOpen = false; + writeIer(port, 0); + _enable(); + + if (port->useFifo) { + _outp(base + UART_FCR, FCR_ENABLE | FCR_RX_RESET | FCR_TX_RESET); + _outp(base + UART_FCR, 0); + } + + _outp(base + UART_MCR, (port->keepDtrOnClose && port->dtrOn) ? MCR_DTR : 0); + clearUartStatus(base); + + unhookIsr(port); + freeBuffer(&port->rxBuf); + freeBuffer(&port->txBuf); +} + + +// ----------------------------------------------------------------------- +// comBaseIrq - GETBASEIRQ for a COM port: base in the low word, IRQ in +// the high word +// ----------------------------------------------------------------------- +static int32_t comBaseIrq(int16_t commId) { + uint16_t base; + uint8_t irq; + + if (commId < 0 || commId >= MAX_PORTS) { + return BASEIRQ_NO_PORT; + } + + if (ports[commId].isOpen) { + base = ports[commId].comDeb.baseAddr; + irq = ports[commId].irq; + } else { + readPortConfig(commId, &base, &irq); + } + return MAKELONG(base, irq); +} + + +// ----------------------------------------------------------------------- +// comIniKey - Build "COMn" for a port +// ----------------------------------------------------------------------- +static void comIniKey(char FAR *key, int16_t commId, const char FAR *suffix) { + lstrcpy(key, COM_KEY_PREFIX); + key[COM_KEY_DIGIT] = (char)(key[COM_KEY_DIGIT] + commId); + lstrcat(key, suffix); +} + + +// ----------------------------------------------------------------------- +// commNotifyWndProc - Ordinal 101 stub +// +// The stock COMM.DRV and CyberCom export an undocumented function at +// ordinal 101; CyberCom's is a plain RETF with no parameters. // ----------------------------------------------------------------------- int FAR PASCAL _export commNotifyWndProc(void) { - dbgStr("KPCOMM: ord101 called\r\n"); return 0; } // ----------------------------------------------------------------------- -// csetbrk - Assert break signal (ordinal 13) +// commWriteString - Queue a block of bytes (ordinal 19) +// +// Returns the number of bytes queued; CE_TXFULL is set if not all fit. +// ----------------------------------------------------------------------- +int16_t FAR PASCAL _export commWriteString(int16_t commId, void FAR *buf, int16_t len) { + PortStateT *port; + LptStateT *lpt; + + lpt = getOpenLpt(commId); + if (lpt) { + return lptWrite(lpt, (const uint8_t FAR *)buf, len); + } + + port = getOpenPort(commId); + if (!port) { + return DRV_ERROR; + } + if (len <= 0) { + return 0; + } + return (int16_t)writeTx(port, (const uint8_t FAR *)buf, (uint16_t)len); +} + + +// ----------------------------------------------------------------------- +// csetbrk - Set break (ordinal 13) // ----------------------------------------------------------------------- int16_t FAR PASCAL _export csetbrk(int16_t commId) { - PortStateT *port; - uint8_t lcr; - - dbgInt16("KPCOMM: csetbrk Id", commId); - - if (commId < 0 || commId >= MAX_PORTS) { - return -1; - } - - port = &ports[commId]; - if (!port->isOpen) { - return -1; - } - - lcr = (uint8_t)_inp(port->baseAddr + UART_LCR); - _outp(port->baseAddr + UART_LCR, lcr | LCR_SBRK); - - port->breakState = TRUE; - return 0; + return setBreak(commId, true); } // ----------------------------------------------------------------------- -// ctx - Send single character with priority (ordinal 6) +// ctx - Send one character ahead of the queue (ordinal 6) +// +// Returns CTX_BUSY if the previous one has not gone out yet (DDAG), or if +// a modem line guarded by a finite DCB timeout stayed low (CE_*TO set). // ----------------------------------------------------------------------- -int16_t FAR PASCAL _export ctx(int16_t commId, int16_t ch) { +uint16_t FAR PASCAL _export ctx(int16_t commId, int16_t ch) { PortStateT *port; + LptStateT *lpt; + uint16_t result; + uint16_t lineErrors; + uint8_t data; - dbgInt16("KPCOMM: ctx Id", commId); + data = (uint8_t)ch; - if (commId < 0 || commId >= MAX_PORTS) { - return -1; + lpt = getOpenLpt(commId); + if (lpt) { + return (lptWrite(lpt, &data, 1) == 1) ? 0 : CTX_BUSY; } - port = &ports[commId]; - if (!port->isOpen) { - return -1; + port = getOpenPort(commId); + if (!port) { + return CE_MODE; + } + + lineErrors = waitTxLines(port); + if (lineErrors) { + addErrors(port, lineErrors); + return CTX_BUSY; } _disable(); - port->txImmediate = ch; - // Enable THRE interrupt to get it sent - _outp(port->baseAddr + UART_IER, (uint8_t)(_inp(port->baseAddr + UART_IER) | IER_THRE)); + if (port->txImmediate != NO_CHAR) { + result = CTX_BUSY; + } else { + port->txImmediate = data; + startTx(port); + result = 0; + } _enable(); + return result; +} + + +#if DBG_ENABLED +// ----------------------------------------------------------------------- +// dbgHex16 - Log "label: 0xNNNN" +// ----------------------------------------------------------------------- +void dbgHex16(const char FAR *label, uint16_t val) { + static const char hex[] = "0123456789ABCDEF"; + char buf[DBG_LINE_MAX]; + int16_t i; + int16_t shift; + + for (i = 0; i < DBG_LABEL_MAX && label[i]; i++) { + buf[i] = label[i]; + } + buf[i++] = ':'; + buf[i++] = ' '; + buf[i++] = '0'; + buf[i++] = 'x'; + for (shift = 12; shift >= 0; shift -= HEX_DIGIT_BITS) { + buf[i++] = hex[(val >> shift) & 0xF]; + } + buf[i++] = '\r'; + buf[i++] = '\n'; + buf[i] = '\0'; + OutputDebugString(buf); +} + + +// ----------------------------------------------------------------------- +// dbgStr - Log a string +// ----------------------------------------------------------------------- +void dbgStr(const char FAR *msg) { + OutputDebugString(msg); +} +#endif + + +// ----------------------------------------------------------------------- +// dcbToLine - Validate a DCB's line settings and compute divisor and LCR +// +// Returns 0 or IE_BAUDRATE / IE_BYTESIZE / IE_DEFAULT. LCR_STB gives +// 1.5 stop bits with 5 data bits and 2 otherwise. +// ----------------------------------------------------------------------- +static int16_t dcbToLine(const DCB FAR *dcb, uint16_t FAR *divisor, uint8_t FAR *lcr) { + uint8_t value; + + *divisor = baudDivisor(dcb->BaudRate); + if (*divisor == 0) { + return IE_BAUDRATE; + } + + if (dcb->ByteSize < MIN_BYTE_SIZE || dcb->ByteSize > MAX_BYTE_SIZE) { + return IE_BYTESIZE; + } + value = (uint8_t)(dcb->ByteSize - MIN_BYTE_SIZE); + + switch (dcb->StopBits) { + case ONESTOPBIT: + break; + case ONE5STOPBITS: + case TWOSTOPBITS: + value |= LCR_STB; + break; + default: + return IE_DEFAULT; + } + + switch (dcb->Parity) { + case NOPARITY: + break; + case ODDPARITY: + value |= LCR_PEN; + break; + case EVENPARITY: + value |= LCR_PEN | LCR_EPS; + break; + case MARKPARITY: + value |= LCR_PEN | LCR_SPAR; + break; + case SPACEPARITY: + value |= LCR_PEN | LCR_EPS | LCR_SPAR; + break; + default: + return IE_DEFAULT; + } + + *lcr = value; return 0; } // ----------------------------------------------------------------------- -// detect16550 - Test if the UART at baseAddr is a 16550 with working FIFOs +// detect16550 - True if the UART is a 16550A with working FIFOs // -// Returns 1 if 16550A (working FIFO), 0 otherwise. +// Leaves the FIFOs disabled; enableFifo turns them on with the trigger. // ----------------------------------------------------------------------- -int16_t detect16550(uint16_t baseAddr) { +static bool detect16550(uint16_t base) { uint8_t iir; - // Try to enable FIFOs - _outp(baseAddr + UART_FCR, FCR_ENABLE); + _outp(base + UART_FCR, FCR_ENABLE); + iir = (uint8_t)_inp(base + UART_IIR); + _outp(base + UART_FCR, 0); - // Read IIR -- bits 7:6 should be set if FIFOs are enabled - iir = (uint8_t)_inp(baseAddr + UART_IIR); - - // Disable FIFOs for now (we'll re-enable properly later) - _outp(baseAddr + UART_FCR, 0); - - // 16550A has both bits 7:6 set when FIFOs enabled - // Plain 16550 (broken FIFO) only sets bit 7 - return ((iir & IIR_FIFO_MASK) == IIR_FIFO_MASK) ? 1 : 0; + // 16550A sets both IIR bits 7:6; the original 16550 (broken FIFO) only bit 7 + return (iir & IIR_FIFO_MASK) == IIR_FIFO_MASK; } // ----------------------------------------------------------------------- -// enableFifo - Enable 16550 FIFOs with configurable trigger level +// drainTx - Wait for queued output to leave the UART (for trmcom) // -// Uses port->fifoTrigger (set from SYSTEM.INI COMnRxTRIGGER, default 8). -// Respects port->fifoEnabled (set from SYSTEM.INI COMnFIFO, default 1). +// Fails at once if a modem line guarded by a finite DCB timeout stays low +// past it; otherwise gives up after DRAIN_TIMEOUT_TICKS without progress, +// e.g. when held by XOFF or flow control. Counts BIOS tick changes rather +// than comparing tick values, so midnight rollover cannot cause a false +// timeout. GetTickCount lives in USER, which COMM.DRV cannot import. +// Returns false on timeout. // ----------------------------------------------------------------------- -void enableFifo(PortStateT *port) { - if (!port->is16550 || !port->fifoEnabled) { - return; +static bool drainTx(PortStateT *port) { + uint16_t base; + uint16_t lastCount; + uint16_t lastTick; + uint16_t idleTicks; + uint16_t count; + uint16_t tick; + + if (biosDataSelector() == 0) { + return port->comDeb.txCount == 0; + } + if (port->comDeb.txCount > 0 && waitTxLines(port) != 0) { + return false; } - // Enable + reset both FIFOs + configured RX trigger level - _outp(port->baseAddr + UART_FCR, FCR_ENABLE | FCR_RX_RESET | FCR_TX_RESET | port->fifoTrigger); + base = port->comDeb.baseAddr; + lastCount = port->comDeb.txCount; + lastTick = readBiosWord(BDA_TICK_COUNT); + idleTicks = 0; + + for (;;) { + count = port->comDeb.txCount; + if (count == 0 && port->flowChar == NO_CHAR && port->txImmediate == NO_CHAR && (_inp(base + UART_LSR) & LSR_TEMT)) { + return true; + } + + if (count != lastCount) { + lastCount = count; + idleTicks = 0; + } + + tick = readBiosWord(BDA_TICK_COUNT); + if (tick != lastTick) { + lastTick = tick; + idleTicks++; + if (idleTicks > DRAIN_TIMEOUT_TICKS) { + return false; + } + } + } } // ----------------------------------------------------------------------- -// enableNotification - Register window for WM_COMMNOTIFY (ordinal 100) -// -// Retained as a no-op stub for API compatibility. Stores threshold -// values but nothing reads them -- polling replaces notifications. +// enableFifo - Enable and reset the FIFOs with the configured RX trigger // ----------------------------------------------------------------------- -int16_t FAR PASCAL _export enableNotification(int16_t commId, HWND hwnd, int16_t rxThresh, int16_t txThresh) { +static void enableFifo(PortStateT *port) { + if (port->useFifo) { + _outp(port->comDeb.baseAddr + UART_FCR, FCR_ENABLE | FCR_RX_RESET | FCR_TX_RESET | port->comDeb.fifoTrigger); + } +} + + +// ----------------------------------------------------------------------- +// enableNotification - Register a window for WM_COMMNOTIFY (ordinal 100) +// +// hwnd NULL disables notifications; a trigger of -1 disables that kind. +// Posts at once if a condition already holds. Returns TRUE/FALSE. +// ----------------------------------------------------------------------- +int16_t FAR PASCAL _export enableNotification(int16_t commId, HWND hwnd, int16_t rxTrigger, int16_t txTrigger) { PortStateT *port; - if (commId < 0 || commId >= MAX_PORTS) { + port = getOpenPort(commId); + if (!port) { + return FALSE; + } + if (hwnd && !resolvePostMessage()) { return FALSE; } - port = &ports[commId]; - if (!port->isOpen) { - return FALSE; - } - - port->hwndNotify = hwnd; - port->rxNotifyThresh = rxThresh; - port->txNotifyThresh = txThresh; + _disable(); + port->hwndNotify = hwnd; + port->rxNotifyTrigger = rxTrigger; + port->txNotifyTrigger = txTrigger; + port->notifySent = 0; + checkNotify(port); + _enable(); return TRUE; } // ----------------------------------------------------------------------- -// freeBuffers - Free ring buffers for a port +// freeBuffer - Release a buffer from allocBuffer (no-op if empty) // ----------------------------------------------------------------------- -static void freeBuffers(PortStateT *port) { - if (port->rxBufH) { - GlobalUnlock(port->rxBufH); - GlobalFree(port->rxBufH); - port->rxBufH = 0; - port->rxBuf = NULL; +static void freeBuffer(BufferT FAR *buf) { + if (!buf->handle) { + return; } - if (port->txBufH) { - GlobalUnlock(port->txBufH); - GlobalFree(port->txBufH); - port->txBufH = 0; - port->txBuf = NULL; - } + GlobalPageUnlock(buf->handle); + GlobalUnlock(buf->handle); + GlobalFree(buf->handle); + buf->handle = NULL; + buf->data = NULL; } // ----------------------------------------------------------------------- -// getdcb - Return pointer to internal DCB (ordinal 15) -// -// Takes int16_t commId (2 bytes, RETF 2). Returns DCB FAR * in DX:AX. -// The caller (USER.EXE / COMMTASK.DLL) copies from this pointer. -// CyberCom confirms: RETF 2, return pointer. +// getdcb - Return a pointer to the port's DCB (ordinal 15) // ----------------------------------------------------------------------- DCB FAR * FAR PASCAL _export getdcb(int16_t commId) { PortStateT *port; + LptStateT *lpt; - dbgInt16("KPCOMM: getdcb Id", commId); - - if (commId < 0 || commId >= MAX_PORTS) { - dbgStr("KPCOMM: getdcb bad id\r\n"); - return NULL; + lpt = getOpenLpt(commId); + if (lpt) { + return &lpt->dcb; } - port = &ports[commId]; - if (!port->isOpen) { - dbgStr("KPCOMM: getdcb not open\r\n"); + port = getOpenPort(commId); + if (!port) { return NULL; } - - dbgHex16("KPCOMM: getdcb isrHits", isrHitCount); - dbgStr("KPCOMM: getdcb OK\r\n"); return &port->dcb; } // ----------------------------------------------------------------------- -// inicom - Open a COM port (ordinal 1) +// getOpenPort - Port state for an open COM cid, or NULL +// ----------------------------------------------------------------------- +static PortStateT *getOpenPort(int16_t commId) { + if (commId < 0 || commId >= MAX_PORTS || !ports[commId].isOpen) { + return NULL; + } + return &ports[commId]; +} + + +// ----------------------------------------------------------------------- +// hexDigit - Value of a hex digit, or -1 +// ----------------------------------------------------------------------- +static int16_t hexDigit(char ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + if (ch >= 'A' && ch <= 'F') { + return ch - 'A' + 10; + } + if (ch >= 'a' && ch <= 'f') { + return ch - 'a' + 10; + } + return -1; +} + + +// ----------------------------------------------------------------------- +// inicom - Open a port (ordinal 1) // -// Opens the port specified in dcb->Id, allocates ring buffers, hooks ISR, -// enables interrupts, and applies DCB settings. -// -// rxBuf and rxSize are ignored (legacy params); we allocate our own buffers. -// -// Returns commId (0-3) on success, negative error code on failure. +// The cid is dcb->Id (0-3 = COM1-COM4, 0x80-0x82 = LPT1-LPT3). Settings +// are validated before any hardware is touched. The queues get a default +// size here; USER calls setque right after with the application's sizes. +// Returns 0 or an IE_* code (DDAG). // ----------------------------------------------------------------------- int16_t FAR PASCAL _export inicom(DCB FAR *dcb) { - int16_t commId; PortStateT *port; - uint16_t rxBufSize; - uint16_t txBufSize; - uint8_t mcr; - - dbgStr("KPCOMM: inicom enter\r\n"); - - // Dump raw stack to see actual parameters passed by caller - { - uint16_t bpVal; - uint16_t ssVal; - void FAR *stkPtr; - _asm mov bpVal, bp - _asm mov ssVal, ss - stkPtr = (void FAR *)((uint32_t)ssVal << 16 | (bpVal + 6)); - dbgHex16("KPCOMM: inicom SS", ssVal); - dbgHex16("KPCOMM: inicom BP", bpVal); - dbgDump("KPCOMM: inicom stk", stkPtr, 24); - } + int16_t commId; + int16_t err; + uint16_t base; + uint16_t divisor; + uint16_t size; + uint8_t irq; + uint8_t lcr; if (!dcb) { - dbgStr("KPCOMM: inicom NULL dcb\r\n"); return IE_DEFAULT; } - dbgFarPtr("KPCOMM: inicom dcb", (void FAR *)dcb); - dbgDump("KPCOMM: inicom raw", (void FAR *)dcb, 16); - dbgHex16("KPCOMM: sizeof(DCB)", (uint16_t)sizeof(DCB)); - commId = dcb->Id; - dbgInt16("KPCOMM: inicom Id", commId); - dbgHex16("KPCOMM: inicom BaudRate", dcb->BaudRate); - dbgHex16("KPCOMM: inicom ByteSize", (uint16_t)dcb->ByteSize); - dbgHex16("KPCOMM: inicom Parity", (uint16_t)dcb->Parity); - dbgHex16("KPCOMM: inicom StopBits", (uint16_t)dcb->StopBits); + dbgHex16("KPCOMM: inicom id", (uint16_t)commId); + dbgHex16("KPCOMM: inicom CtsTimeout", dcb->CtsTimeout); + dbgHex16("KPCOMM: inicom DsrTimeout", dcb->DsrTimeout); + dbgHex16("KPCOMM: inicom RlsTimeout", dcb->RlsTimeout); - if (commId < 0 || commId >= MAX_PORTS) { - dbgStr("KPCOMM: inicom IE_BADID\r\n"); + if (isLptId(commId)) { + return lptOpen(dcb); + } + if (commId >= MAX_PORTS) { return IE_BADID; } port = &ports[commId]; if (port->isOpen) { - dbgStr("KPCOMM: inicom IE_OPEN\r\n"); return IE_OPEN; } - // Initialize port state - initPortState(port, commId); - dbgHex16("KPCOMM: inicom baseAddr", port->baseAddr); - dbgHex16("KPCOMM: inicom irq", (uint16_t)port->irq); - - // Read buffer sizes from SYSTEM.INI if available (COMnBuffer key) - { - char bufKey[12]; - bufKey[0] = 'C'; - bufKey[1] = 'O'; - bufKey[2] = 'M'; - bufKey[3] = (char)('1' + commId); - bufKey[4] = 'B'; - bufKey[5] = 'u'; - bufKey[6] = 'f'; - bufKey[7] = 'f'; - bufKey[8] = 'e'; - bufKey[9] = 'r'; - bufKey[10] = '\0'; - rxBufSize = readSystemIni("386Enh", bufKey, DEFAULT_RX_SIZE); + err = dcbToLine(dcb, &divisor, &lcr); + if (err != 0) { + return err; } - txBufSize = rxBufSize; - dbgHex16("KPCOMM: inicom rxBufSize", rxBufSize); - // Allocate ring buffers - if (initBuffers(port, rxBufSize, txBufSize) != 0) { - dbgStr("KPCOMM: inicom IE_MEMORY\r\n"); - return IE_MEMORY; - } - dbgStr("KPCOMM: inicom buffers OK\r\n"); - - // Detect 16550 FIFO - port->is16550 = (uint8_t)detect16550(port->baseAddr); - dbgHex16("KPCOMM: inicom is16550", (uint16_t)port->is16550); - - // Disable UART interrupts and clear all pending conditions BEFORE - // hooking ISR. Without this, a stale interrupt from a previous - // session fires the moment hookIsr unmasks the PIC. The ISR sees - // isOpen=FALSE, skips isrDispatch, sends EOI, but the UART's IRQ - // line stays asserted (level-triggered ISA). Result: infinite - // ISR loop, hard lockup. - _outp(port->baseAddr + UART_IER, 0); - (void)_inp(port->baseAddr + UART_IIR); - (void)_inp(port->baseAddr + UART_LSR); - (void)_inp(port->baseAddr + UART_MSR); - (void)_inp(port->baseAddr + UART_RBR); - - // Hook ISR - if (hookIsr(port) != 0) { - dbgStr("KPCOMM: inicom hookIsr FAIL\r\n"); - freeBuffers(port); + readPortConfig(commId, &base, &irq); + if (!uartPresent(base)) { return IE_HARDWARE; } - dbgStr("KPCOMM: inicom ISR hooked\r\n"); - port->isOpen = TRUE; + initPortState(port, commId, base, irq); + + size = queueSize(commId, DEFAULT_QUEUE_SIZE); + if (!allocBuffer(&port->rxBuf, size) || !allocBuffer(&port->txBuf, size)) { + freeBuffer(&port->rxBuf); + return IE_MEMORY; + } + port->comDeb.rxSize = size; + port->comDeb.txSize = size; + + // Quiesce the UART before the IRQ is unmasked: a condition left over + // from an earlier session would otherwise interrupt before the port is + // marked open and could never be cleared. + _outp(base + UART_IER, 0); + clearUartStatus(base); + + if (hookIsr(port) != 0) { + freeBuffer(&port->rxBuf); + freeBuffer(&port->txBuf); + return IE_HARDWARE; + } - // Enable FIFOs if 16550 detected enableFifo(port); + port->comDeb.msrShadow = (uint8_t)_inp(base + UART_MSR); + port->isOpen = true; + (void)applyDcb(port, dcb, true); - // Apply DCB settings - _fmemcpy(&port->dcb, dcb, sizeof(DCB)); - applyBaudRate(port, dcb->BaudRate); - applyLineParams(port, dcb->ByteSize, dcb->Parity, dcb->StopBits); - - // Set flow control from DCB - if (dcb->fOutxCtsFlow && dcb->fInX) { - port->hsMode = HS_BOTH; - } else if (dcb->fOutxCtsFlow) { - port->hsMode = HS_RTSCTS; - } else if (dcb->fInX) { - port->hsMode = HS_XONXOFF; - } else { - port->hsMode = HS_NONE; - } - - port->xonChar = dcb->XonChar; - port->xoffChar = dcb->XoffChar; - port->xonLim = dcb->XonLim; - port->xoffLim = dcb->XoffLim; - - // Raise DTR and RTS, enable OUT2 (master interrupt gate) - mcr = MCR_DTR | MCR_RTS | MCR_OUT2; - _outp(port->baseAddr + UART_MCR, mcr); - port->dtrState = TRUE; - port->rtsState = TRUE; - - // Clear any pending status - _inp(port->baseAddr + UART_LSR); - port->comDeb.msrShadow = (uint8_t)_inp(port->baseAddr + UART_MSR); - _inp(port->baseAddr + UART_RBR); - - // Populate ComDEB for third-party compatibility - port->comDeb.port = port->baseAddr; - port->comDeb.baudRate = port->baudRate; - port->comDeb.qInSize = port->rxSize; - port->comDeb.qOutSize = port->txSize; - port->comDeb.lcrShadow = (uint8_t)_inp(port->baseAddr + UART_LCR); - port->comDeb.mcrShadow = (uint8_t)_inp(port->baseAddr + UART_MCR); - - // Enable receive and line status interrupts - _outp(port->baseAddr + UART_IER, IER_RDA | IER_LSI | IER_MSI); - - dbgInt16("KPCOMM: inicom OK, ret", commId); - return commId; -} - - -// ----------------------------------------------------------------------- -// initBuffers - Allocate ring buffers via GlobalAlloc -// -// Uses GMEM_FIXED so buffer addresses are stable at interrupt time -// without requiring GlobalLock. Saves handles in PortStateT for -// clean freeing (no GlobalHandle recovery hack). -// ----------------------------------------------------------------------- -static int16_t initBuffers(PortStateT *port, uint16_t rxSz, uint16_t txSz) { - HGLOBAL hRx; - HGLOBAL hTx; - - hRx = GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, (DWORD)rxSz); - if (!hRx) { - return -1; - } - - hTx = GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, (DWORD)txSz); - if (!hTx) { - GlobalFree(hRx); - return -1; - } - - port->rxBufH = hRx; - port->rxBuf = (uint8_t FAR *)GlobalLock(hRx); - port->rxSize = rxSz; - port->rxHead = 0; - port->rxTail = 0; - port->rxCount = 0; - - port->txBufH = hTx; - port->txBuf = (uint8_t FAR *)GlobalLock(hTx); - port->txSize = txSz; - port->txHead = 0; - port->txTail = 0; - port->txCount = 0; + // Raising DTR/RTS may have changed the modem lines (RTS loops back to + // CTS on a null-modem cable). Take a fresh MSR snapshot in the same + // guarded step that enables interrupts so no transition is missed. + _disable(); + clearUartStatus(base); + port->comDeb.msrShadow = (uint8_t)_inp(base + UART_MSR); + writeIer(port, (uint8_t)(port->comDeb.ierShadow | IER_RX)); + updateTxHold(port); + _enable(); + dbgStr("KPCOMM: inicom OK\r\n"); return 0; } // ----------------------------------------------------------------------- -// initPortState - Initialize a port state structure with defaults -// -// Reads COMnBase, COMnIRQ, COMnFIFO, and COMnRxTRIGGER from -// SYSTEM.INI [386Enh] to support non-standard port configurations. +// initPortState - Reset a port's state for a fresh open // ----------------------------------------------------------------------- -static void initPortState(PortStateT *port, int16_t commId) { - uint16_t baseAddr; - uint8_t irq; - uint16_t fifoEnabled; - uint16_t rxTrigger; - +static void initPortState(PortStateT *port, int16_t commId, uint16_t base, uint8_t irq) { _fmemset(port, 0, sizeof(PortStateT)); - port->commId = commId; - - // Read port address and IRQ from SYSTEM.INI (or use defaults) - readPortConfig(commId, &baseAddr, &irq); - port->baseAddr = baseAddr; - port->irq = irq; - - // Read FIFO enable setting (default: enabled) - { - char fifoKey[10]; - fifoKey[0] = 'C'; - fifoKey[1] = 'O'; - fifoKey[2] = 'M'; - fifoKey[3] = (char)('1' + commId); - fifoKey[4] = 'F'; - fifoKey[5] = 'I'; - fifoKey[6] = 'F'; - fifoKey[7] = 'O'; - fifoKey[8] = '\0'; - fifoEnabled = readSystemIni("386Enh", fifoKey, 1); - } - port->fifoEnabled = (uint8_t)(fifoEnabled ? 1 : 0); - - // Read RX trigger level from SYSTEM.INI (default: 8) - { - char trigKey[16]; - trigKey[0] = 'C'; - trigKey[1] = 'O'; - trigKey[2] = 'M'; - trigKey[3] = (char)('1' + commId); - trigKey[4] = 'R'; - trigKey[5] = 'x'; - trigKey[6] = 'T'; - trigKey[7] = 'R'; - trigKey[8] = 'I'; - trigKey[9] = 'G'; - trigKey[10] = 'G'; - trigKey[11] = 'E'; - trigKey[12] = 'R'; - trigKey[13] = '\0'; - rxTrigger = readSystemIni("386Enh", trigKey, 1); - } - switch (rxTrigger) { - case 4: - port->fifoTrigger = FCR_TRIG_4; - break; - case 8: - port->fifoTrigger = FCR_TRIG_8; - break; - case 14: - port->fifoTrigger = FCR_TRIG_14; - break; - case 1: - default: - port->fifoTrigger = FCR_TRIG_1; - break; - } - - port->xonChar = 0x11; // DC1 - port->xoffChar = 0x13; // DC3 - port->xonLim = DEFAULT_RX_SIZE / 4; - port->xoffLim = DEFAULT_RX_SIZE / 4; - - port->txImmediate = -1; - port->rxNotifyThresh = -1; - port->txNotifyThresh = -1; + port->comDeb.baseAddr = base; + port->comDeb.fifoTrigger = rxTriggerBits(commId); + port->irq = irq; + port->useFifo = detect16550(base) && readComIniBool(commId, INI_KEY_FIFO, true); + port->keepDtrOnClose = readComIniBool(commId, INI_KEY_KEEPDTR, false); + port->flowChar = NO_CHAR; + port->txImmediate = NO_CHAR; + port->rxNotifyTrigger = NOTIFY_OFF; + port->txNotifyTrigger = NOTIFY_OFF; } // ----------------------------------------------------------------------- -// primeTx - Prime the transmitter by enabling THRE interrupt -// -// Called after writing data to the TX ring buffer to kick off -// transmission if it's not already running. +// queueSize - Queue size for a request, raised to the COMnBuffer floor // ----------------------------------------------------------------------- -void primeTx(PortStateT *port) { - uint8_t ier; +static uint16_t queueSize(int16_t commId, uint16_t requested) { + uint16_t minSize; - ier = (uint8_t)_inp(port->baseAddr + UART_IER); - if (!(ier & IER_THRE)) { - _outp(port->baseAddr + UART_IER, ier | IER_THRE); + minSize = readComIni(commId, INI_KEY_BUFFER, MIN_QUEUE_SIZE); + if (minSize < MIN_QUEUE_SIZE) { + minSize = MIN_QUEUE_SIZE; } + return (requested > minSize) ? requested : minSize; } // ----------------------------------------------------------------------- -// reactivateOpenCommPorts - Reactivate all ports after task switch (ordinal 18) +// reactivateOpenCommPorts - Take the hardware back after a DOS application +// (ordinal 18) // -// Called by Windows when switching back to this VM. -// Restores full UART state: baud rate, line params (LCR), MCR, FIFOs, -// and re-enables interrupts. A DOS fullscreen app or VM switch may -// have reprogrammed the UART, so we must restore everything. +// The DOS application may have reprogrammed everything, so line settings, +// FIFOs, modem lines, interrupt enables, and PIC masks are all restored +// from the saved state. Every UART is silenced and reprogrammed before +// the ISR takes the ports back, so it never services a UART still in the +// DOS application's configuration. Queued data is kept. // ----------------------------------------------------------------------- void FAR PASCAL _export reactivateOpenCommPorts(void) { - int16_t i; PortStateT *port; - uint8_t mcr; + int16_t i; dbgStr("KPCOMM: reactivate\r\n"); for (i = 0; i < MAX_PORTS; i++) { port = &ports[i]; - if (!port->isOpen) { - continue; + if (port->isOpen) { + _disable(); + writeIer(port, 0); + writeLine(port, baudDivisor(port->dcb.BaudRate), port->comDeb.lcrShadow); + enableFifo(port); + writeMcr(port); + _enable(); } + } - // Restore baud rate (sets DLAB, writes divisor, clears DLAB) - applyBaudRate(port, port->baudRate); + portsSuspended = false; - // Restore line parameters (word length, parity, stop bits) - applyLineParams(port, port->byteSize, port->parity, port->stopBits); - - // Restore MCR (DTR, RTS, OUT2) - mcr = MCR_OUT2; - if (port->dtrState) { - mcr |= MCR_DTR; + for (i = 0; i < MAX_PORTS; i++) { + port = &ports[i]; + if (port->isOpen) { + _disable(); + clearUartStatus(port->comDeb.baseAddr); + port->comDeb.msrShadow = (uint8_t)_inp(port->comDeb.baseAddr + UART_MSR); + writeIer(port, IER_RX); + updateTxHold(port); + _enable(); } - if (port->rtsState) { - mcr |= MCR_RTS; - } - _outp(port->baseAddr + UART_MCR, mcr); + } - // Re-enable FIFOs - enableFifo(port); + reactivateIrqs(); +} - // Re-enable interrupts - _outp(port->baseAddr + UART_IER, IER_RDA | IER_LSI | IER_MSI); - if (port->txCount > 0) { - _outp(port->baseAddr + UART_IER, (uint8_t)(_inp(port->baseAddr + UART_IER) | IER_THRE)); - } - // Unmask IRQ at PIC - { - uint8_t picMask = (uint8_t)_inp(PIC_DATA); - picMask &= ~port->irqMask; - _outp(PIC_DATA, picMask); - } +// ----------------------------------------------------------------------- +// readBiosWord - Read a word from the BIOS data area (0 if unavailable) +// ----------------------------------------------------------------------- +uint16_t readBiosWord(uint16_t offset) { + uint16_t sel; + + sel = biosDataSelector(); + if (sel == 0) { + return 0; + } + return *(volatile uint16_t FAR *)MAKELP(sel, offset); +} + + +// ----------------------------------------------------------------------- +// readComIni - Read COMn from SYSTEM.INI [386Enh] as an integer +// ----------------------------------------------------------------------- +static uint16_t readComIni(int16_t commId, const char FAR *suffix, uint16_t defVal) { + char key[INI_KEY_MAX]; + + comIniKey(key, commId, suffix); + return (uint16_t)GetPrivateProfileInt(INI_SECTION, key, (int)defVal, INI_FILE); +} + + +// ----------------------------------------------------------------------- +// readComIniBool - Read COMn as a SYSTEM.INI Boolean +// +// Accepts 1-9/true/yes/on and 0/false/no/off (any case) by hand, since +// lstrcmpi lives in USER. Returns defVal when missing or unrecognized. +// ----------------------------------------------------------------------- +static bool readComIniBool(int16_t commId, const char FAR *suffix, bool defVal) { + char value[INI_VALUE_MAX]; + + readComIniString(commId, suffix, value); + + switch (value[0]) { + case 't': + case 'T': + case 'y': + case 'Y': + return true; + case '0': + case 'f': + case 'F': + case 'n': + case 'N': + return false; + case 'o': + case 'O': + return value[1] == 'n' || value[1] == 'N'; + default: + return (value[0] >= '1' && value[0] <= '9') ? true : defVal; } } // ----------------------------------------------------------------------- -// readCommString - Read data (ordinal 20, alias for reccom) +// readComIniHex - Read COMn as hex ("3E8", "0x3E8", or "3E8h") +// +// Parsing stops at the first non-hex character. Returns defVal when the +// key is missing or holds no nonzero value. +// ----------------------------------------------------------------------- +static uint16_t readComIniHex(int16_t commId, const char FAR *suffix, uint16_t defVal) { + char value[INI_VALUE_MAX]; + uint16_t result; + int16_t digit; + int16_t i; + + readComIniString(commId, suffix, value); + + i = 0; + if (value[0] == '0' && (value[1] == 'x' || value[1] == 'X')) { + i = HEX_PREFIX_LEN; + } + + result = 0; + for (;;) { + digit = hexDigit(value[i]); + if (digit < 0) { + break; + } + result = (uint16_t)((result << HEX_DIGIT_BITS) | (uint16_t)digit); + i++; + } + + return (result != 0) ? result : defVal; +} + + +// ----------------------------------------------------------------------- +// readComIniString - Read COMn into value (INI_VALUE_MAX bytes; +// empty string if missing) +// ----------------------------------------------------------------------- +static void readComIniString(int16_t commId, const char FAR *suffix, char FAR *value) { + char key[INI_KEY_MAX]; + + comIniKey(key, commId, suffix); + GetPrivateProfileString(INI_SECTION, key, "", value, INI_VALUE_MAX, INI_FILE); +} + + +// ----------------------------------------------------------------------- +// readCommString - Read a block from the receive queue (ordinal 20) +// +// Returns the number of bytes read (0 if none). // ----------------------------------------------------------------------- int16_t FAR PASCAL _export readCommString(int16_t commId, void FAR *buf, int16_t len) { - dbgInt16("KPCOMM: readCommStr Id", commId); - return reccom(commId, buf, len); -} - - -// ----------------------------------------------------------------------- -// readPortConfig - Read port base address and IRQ from SYSTEM.INI -// -// Checks [386Enh] for COMnBase (hex) and COMnIRQ overrides. -// Falls back to standard defaults if not present. -// ----------------------------------------------------------------------- -static void readPortConfig(int16_t commId, uint16_t FAR *baseAddr, uint8_t FAR *irq) { - char key[10]; - char buf[16]; - - // Defaults - *baseAddr = portBase[commId]; - *irq = portIrq[commId]; - - // Check COMnBase (e.g., COM3Base=3E8) - key[0] = 'C'; - key[1] = 'O'; - key[2] = 'M'; - key[3] = (char)('1' + commId); - key[4] = 'B'; - key[5] = 'a'; - key[6] = 's'; - key[7] = 'e'; - key[8] = '\0'; - - GetPrivateProfileString("386Enh", key, "", buf, sizeof(buf), "SYSTEM.INI"); - if (buf[0] != '\0') { - // Parse hex string manually (no strtoul in small model CRT) - uint16_t val = 0; - int16_t i = 0; - char ch; - - while (buf[i] != '\0') { - ch = buf[i]; - val <<= 4; - if (ch >= '0' && ch <= '9') { - val |= (uint16_t)(ch - '0'); - } else if (ch >= 'A' && ch <= 'F') { - val |= (uint16_t)(ch - 'A' + 10); - } else if (ch >= 'a' && ch <= 'f') { - val |= (uint16_t)(ch - 'a' + 10); - } - i++; - } - if (val != 0) { - *baseAddr = val; - } - } - - // Check COMnIRQ (e.g., COM3Irq=5) - key[4] = 'I'; - key[5] = 'r'; - key[6] = 'q'; - key[7] = '\0'; - - { - uint16_t irqVal = readSystemIni("386Enh", key, (uint16_t)*irq); - if (irqVal > 0 && irqVal <= 7) { - *irq = (uint8_t)irqVal; - } - } -} - - -// ----------------------------------------------------------------------- -// readSystemIni - Read an unsigned integer value from SYSTEM.INI -// ----------------------------------------------------------------------- -static uint16_t readSystemIni(const char FAR *section, const char FAR *key, uint16_t defVal) { - return (uint16_t)GetPrivateProfileInt(section, key, (int)defVal, "SYSTEM.INI"); -} - - -// ----------------------------------------------------------------------- -// reccom - Read from RX ring buffer (ordinal 4) -// -// Copies up to len bytes from the receive ring buffer into buf. -// Returns number of bytes read, or negative error code. -// ----------------------------------------------------------------------- -int16_t FAR PASCAL _export reccom(int16_t commId, void FAR *buf, int16_t len) { - PortStateT *port; - uint8_t FAR *dst; - uint16_t bytesRead; - - dbgInt16("KPCOMM: reccom Id", commId); - - if (commId < 0 || commId >= MAX_PORTS) { - return -1; - } - - port = &ports[commId]; - if (!port->isOpen) { - return -1; - } - - dst = (uint8_t FAR *)buf; - bytesRead = 0; - - // Block copy from ring buffer, splitting at wrap point. - // Two _fmemcpy calls replace per-byte loop with far pointer overhead. - _disable(); - { - uint16_t avail; - uint16_t chunk; - - avail = port->rxCount; - if (avail > (uint16_t)len) { - avail = (uint16_t)len; - } - - if (avail > 0) { - // First chunk: tail to end of buffer (or avail, whichever is smaller) - chunk = port->rxSize - port->rxTail; - if (chunk > avail) { - chunk = avail; - } - _fmemcpy(dst, port->rxBuf + port->rxTail, chunk); - dst += chunk; - bytesRead = chunk; - - // Second chunk: wrap around to start of buffer - if (bytesRead < avail) { - chunk = avail - bytesRead; - _fmemcpy(dst, port->rxBuf, chunk); - bytesRead += chunk; - } - - port->rxTail += bytesRead; - if (port->rxTail >= port->rxSize) { - port->rxTail -= port->rxSize; - } - port->rxCount -= bytesRead; - } - } - _enable(); - - // If flow control was asserted and buffer has drained, deassert - if (port->rxStopped && port->rxCount <= port->xonLim) { - _disable(); - port->rxStopped = FALSE; - - if (port->hsMode == HS_XONXOFF || port->hsMode == HS_BOTH) { - port->txImmediate = port->xonChar; - _outp(port->baseAddr + UART_IER, (uint8_t)(_inp(port->baseAddr + UART_IER) | IER_THRE)); - } - if (port->hsMode == HS_RTSCTS || port->hsMode == HS_BOTH) { - _outp(port->baseAddr + UART_MCR, (uint8_t)(_inp(port->baseAddr + UART_MCR) | MCR_RTS)); - } - _enable(); - } - - // Sync ComDEB queue counts - port->comDeb.qInCount = port->rxCount; - port->comDeb.qInTail = port->rxTail; - - return bytesRead; -} - - -// ----------------------------------------------------------------------- -// setcom - Apply DCB settings to hardware (ordinal 2) -// -// Takes DCB FAR * (4 bytes, RETF 4). CommId is in dcb->Id. -// Both USER.EXE and COMMTASK.DLL push 4 bytes for this call. -// ----------------------------------------------------------------------- -int16_t FAR PASCAL _export setcom(DCB FAR *dcb) { - int16_t commId; PortStateT *port; - dbgStr("KPCOMM: setcom enter\r\n"); + if (getOpenLpt(commId)) { + return 0; + } + + port = getOpenPort(commId); + if (!port) { + return DRV_ERROR; + } + if (len <= 0) { + return 0; + } + return (int16_t)readRx(port, (uint8_t FAR *)buf, (uint16_t)len); +} + + +// ----------------------------------------------------------------------- +// readPortConfig - Base address and IRQ for a COM port +// +// DDAG: the BIOS data area wins; COMnBase only fills a missing entry. +// COMnIRQ overrides the standard IRQ. "IRQ 2" means IRQ 9 on AT +// hardware. Out-of-range IRQs become 0, which hookIsr rejects. +// ----------------------------------------------------------------------- +static void readPortConfig(int16_t commId, uint16_t FAR *base, uint8_t FAR *irq) { + uint16_t irqVal; + + *base = readBiosWord(BDA_COM_BASES + commId * sizeof(uint16_t)); + if (*base == 0) { + *base = readComIniHex(commId, INI_KEY_BASE, defaultBase[commId]); + } + + irqVal = readComIni(commId, INI_KEY_IRQ, defaultIrq[commId]); + if (irqVal == IRQ_CASCADE) { + irqVal = IRQ_CASCADE_TARGET; + } + *irq = (irqVal < IRQ_COUNT) ? (uint8_t)irqVal : 0; +} + + +// ----------------------------------------------------------------------- +// readRx - Move up to len bytes from the receive queue to dst +// +// Single consumer: only this side moves rxTail, and the ISR only grows +// rxCount, so the snapshot taken here stays valid. The copy runs with +// interrupts enabled; only the count update is guarded. +// ----------------------------------------------------------------------- +static uint16_t readRx(PortStateT *port, uint8_t FAR *dst, uint16_t len) { + uint8_t FAR *buf; + uint16_t size; + uint16_t tail; + uint16_t avail; + uint16_t chunk; + + avail = port->comDeb.rxCount; + if (avail > len) { + avail = len; + } + if (avail == 0) { + return 0; + } + + buf = port->rxBuf.data; + size = port->comDeb.rxSize; + tail = port->comDeb.rxTail; + chunk = size - tail; + if (chunk > avail) { + chunk = avail; + } + _fmemcpy(dst, buf + tail, chunk); + if (chunk < avail) { + _fmemcpy(dst + chunk, buf, avail - chunk); + tail = avail - chunk; + } else { + // tail + chunk <= size, so this cannot overflow 16 bits + tail += chunk; + if (tail == size) { + tail = 0; + } + } + port->comDeb.rxTail = tail; + + _disable(); + port->comDeb.rxCount -= avail; + updateRxFlow(port); + refreshNotify(port, CN_RECEIVE); + _enable(); + + return avail; +} + + +// ----------------------------------------------------------------------- +// reccom - Read one byte (ordinal 4) +// +// Returns the byte, RECCOM_NO_DATA (-2) if the queue is empty, or -1 on +// error (DDAG). +// ----------------------------------------------------------------------- +int16_t FAR PASCAL _export reccom(int16_t commId) { + PortStateT *port; + uint8_t data; + + if (getOpenLpt(commId)) { + return RECCOM_NO_DATA; + } + + port = getOpenPort(commId); + if (!port) { + return DRV_ERROR; + } + if (readRx(port, &data, 1) == 0) { + return RECCOM_NO_DATA; + } + return data; +} + + +// ----------------------------------------------------------------------- +// resetRx - Empty the receive queue and release RX throttling +// (interrupts disabled) +// ----------------------------------------------------------------------- +static void resetRx(PortStateT *port) { + port->comDeb.rxHead = 0; + port->comDeb.rxTail = 0; + port->comDeb.rxCount = 0; + port->eofSeen = false; + updateRxFlow(port); +} + + +// ----------------------------------------------------------------------- +// resetTx - Empty the transmit queue (interrupts disabled) +// ----------------------------------------------------------------------- +static void resetTx(PortStateT *port) { + port->comDeb.txHead = 0; + port->comDeb.txTail = 0; + port->comDeb.txCount = 0; +} + + +// ----------------------------------------------------------------------- +// resolvePostMessage - Look up USER's PostMessage on first use +// +// COMM.DRV loads before USER, so it cannot import USER statically. +// ----------------------------------------------------------------------- +static bool resolvePostMessage(void) { + HMODULE user; + + if (!postMessageProc) { + user = GetModuleHandle("USER"); + if (user) { + postMessageProc = (PostMessageProcT)GetProcAddress((HINSTANCE)user, "PostMessage"); + } + } + return postMessageProc != NULL; +} + + +// ----------------------------------------------------------------------- +// rxTriggerBits - FCR trigger bits for COMnRxTRIGGER (1, 4, 8, or 14 bytes) +// +// Values the 16550A cannot do fall back to DEFAULT_TRIGGER. +// ----------------------------------------------------------------------- +static uint8_t rxTriggerBits(int16_t commId) { + int16_t code; + + code = triggerCode(readComIni(commId, INI_KEY_TRIGGER, DEFAULT_TRIGGER)); + if (code < 0) { + code = triggerCode(DEFAULT_TRIGGER); + } + return (uint8_t)(code << FCR_TRIG_SHIFT); +} + + +// ----------------------------------------------------------------------- +// setBreak - Set or clear the break condition (csetbrk/cclrbrk) +// ----------------------------------------------------------------------- +static int16_t setBreak(int16_t commId, bool on) { + PortStateT *port; + + port = getOpenPort(commId); + if (!port) { + return DRV_ERROR; + } + + if (on) { + port->comDeb.lcrShadow = (uint8_t)(port->comDeb.lcrShadow | LCR_SBRK); + } else { + port->comDeb.lcrShadow = (uint8_t)(port->comDeb.lcrShadow & ~LCR_SBRK); + } + _outp(port->comDeb.baseAddr + UART_LCR, port->comDeb.lcrShadow); + return 0; +} + + +// ----------------------------------------------------------------------- +// setcom - Apply a new DCB (ordinal 2) +// +// Takes only the DCB (RETF 4); the cid is dcb->Id. Returns 0 or IE_*. +// ----------------------------------------------------------------------- +int16_t FAR PASCAL _export setcom(DCB FAR *dcb) { + LptStateT *lpt; + int16_t commId; if (!dcb) { - dbgStr("KPCOMM: setcom NULL dcb\r\n"); - return -1; + return IE_DEFAULT; } commId = dcb->Id; - dbgInt16("KPCOMM: setcom Id", commId); - dbgHex16("KPCOMM: setcom BaudRate", dcb->BaudRate); - dbgHex16("KPCOMM: setcom ByteSize", (uint16_t)dcb->ByteSize); - dbgHex16("KPCOMM: setcom Parity", (uint16_t)dcb->Parity); - dbgHex16("KPCOMM: setcom StopBits", (uint16_t)dcb->StopBits); + dbgHex16("KPCOMM: setcom baud", dcb->BaudRate); + lpt = getOpenLpt(commId); + if (lpt) { + _fmemcpy(&lpt->dcb, dcb, sizeof(DCB)); + return 0; + } + + if (commId >= MAX_PORTS && !isLptId(commId)) { + return IE_BADID; + } + if (!getOpenPort(commId)) { + return IE_NOPEN; + } + return applyDcb(&ports[commId], dcb, false); +} + + +// ----------------------------------------------------------------------- +// setque - Size the receive and transmit queues (ordinal 3) +// +// Takes (cid, qdb FAR *) -- RETF 6. Only the requested sizes are used +// (raised to the COMnBuffer floor); USER's queue memory is ignored in +// favor of our own page-locked buffers. Buffers are replaced only if +// the size changes, and allocation happens before interrupts are +// disabled. Both queues are emptied. Returns 0 or IE_*. +// ----------------------------------------------------------------------- +int16_t FAR PASCAL _export setque(int16_t commId, QdbT FAR *qdb) { + PortStateT *port; + BufferT newRx; + BufferT newTx; + BufferT oldRx; + BufferT oldTx; + uint16_t rxSize; + uint16_t txSize; + + if (isLptId(commId)) { + return getOpenLpt(commId) ? 0 : IE_NOPEN; + } if (commId < 0 || commId >= MAX_PORTS) { - dbgStr("KPCOMM: setcom IE_BADID\r\n"); return IE_BADID; } port = &ports[commId]; if (!port->isOpen) { - dbgStr("KPCOMM: setcom port not open\r\n"); - return -1; + return IE_NOPEN; + } + if (!qdb) { + return IE_DEFAULT; } - // Save DCB - _fmemcpy(&port->dcb, dcb, sizeof(DCB)); + rxSize = queueSize(commId, (uint16_t)qdb->rxSize); + txSize = queueSize(commId, (uint16_t)qdb->txSize); + dbgHex16("KPCOMM: setque rx", rxSize); + dbgHex16("KPCOMM: setque tx", txSize); + + newRx = port->rxBuf; + newTx = port->txBuf; + if (rxSize != port->comDeb.rxSize && !allocBuffer(&newRx, rxSize)) { + return IE_MEMORY; + } + if (txSize != port->comDeb.txSize && !allocBuffer(&newTx, txSize)) { + if (newRx.handle != port->rxBuf.handle) { + freeBuffer(&newRx); + } + return IE_MEMORY; + } - // Apply hardware settings _disable(); - - applyBaudRate(port, dcb->BaudRate); - applyLineParams(port, dcb->ByteSize, dcb->Parity, dcb->StopBits); - - // Update flow control - if (dcb->fOutxCtsFlow && dcb->fInX) { - port->hsMode = HS_BOTH; - } else if (dcb->fOutxCtsFlow) { - port->hsMode = HS_RTSCTS; - } else if (dcb->fInX) { - port->hsMode = HS_XONXOFF; - } else { - port->hsMode = HS_NONE; - } - - port->xonChar = dcb->XonChar; - port->xoffChar = dcb->XoffChar; - port->xonLim = dcb->XonLim; - port->xoffLim = dcb->XoffLim; - - // Sync ComDEB shadow fields - port->comDeb.baudRate = port->baudRate; - port->comDeb.lcrShadow = (uint8_t)_inp(port->baseAddr + UART_LCR); - port->comDeb.mcrShadow = (uint8_t)_inp(port->baseAddr + UART_MCR); - + oldRx = port->rxBuf; + oldTx = port->txBuf; + port->rxBuf = newRx; + port->txBuf = newTx; + port->comDeb.rxSize = rxSize; + port->comDeb.txSize = txSize; + resetRx(port); + resetTx(port); + refreshNotify(port, CN_RECEIVE); _enable(); - dbgStr("KPCOMM: setcom OK\r\n"); + if (oldRx.handle != newRx.handle) { + freeBuffer(&oldRx); + } + if (oldTx.handle != newTx.handle) { + freeBuffer(&oldTx); + } return 0; } // ----------------------------------------------------------------------- -// setque - Resize RX/TX buffers (ordinal 3) +// sndcom - Queue one byte (ordinal 5) // -// Caller passes port ID and requested RX/TX buffer sizes. -// We manage our own GlobalAlloc'd buffers. +// Returns 0, or nonzero if it could not be queued (DDAG). // ----------------------------------------------------------------------- -int16_t FAR PASCAL _export setque(int16_t commId, int16_t rxSz, int16_t txSz) { +uint16_t FAR PASCAL _export sndcom(int16_t commId, int16_t ch) { PortStateT *port; + LptStateT *lpt; + uint8_t data; - dbgInt16("KPCOMM: setque Id", commId); - dbgInt16("KPCOMM: setque rxSz", rxSz); - dbgInt16("KPCOMM: setque txSz", txSz); + data = (uint8_t)ch; - if (commId < 0 || commId >= MAX_PORTS) { - dbgStr("KPCOMM: setque bad id\r\n"); - return -1; + lpt = getOpenLpt(commId); + if (lpt) { + return (lptWrite(lpt, &data, 1) == 1) ? 0 : SNDCOM_FAILED; } - port = &ports[commId]; - if (!port->isOpen) { - dbgStr("KPCOMM: setque port not open\r\n"); - return -1; + port = getOpenPort(commId); + if (!port) { + return SNDCOM_FAILED; } - - // Allocate new buffers BEFORE disabling interrupts. - // GlobalAlloc/GlobalFree can deadlock under CLI if the heap - // needs compaction or DPMI services. - { - HGLOBAL newRxH; - HGLOBAL newTxH; - uint8_t FAR *newRxBuf; - uint8_t FAR *newTxBuf; - HGLOBAL oldRxH; - HGLOBAL oldTxH; - - newRxH = GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, (DWORD)(uint16_t)rxSz); - if (!newRxH) { - dbgStr("KPCOMM: setque IE_MEMORY\r\n"); - return IE_MEMORY; - } - newTxH = GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, (DWORD)(uint16_t)txSz); - if (!newTxH) { - GlobalFree(newRxH); - dbgStr("KPCOMM: setque IE_MEMORY\r\n"); - return IE_MEMORY; - } - newRxBuf = (uint8_t FAR *)GlobalLock(newRxH); - newTxBuf = (uint8_t FAR *)GlobalLock(newTxH); - - // Swap buffers with interrupts disabled -- ISR sees - // consistent pointers at all times - _disable(); - oldRxH = port->rxBufH; - oldTxH = port->txBufH; - port->rxBufH = newRxH; - port->rxBuf = newRxBuf; - port->rxSize = (uint16_t)rxSz; - port->rxHead = 0; - port->rxTail = 0; - port->rxCount = 0; - port->txBufH = newTxH; - port->txBuf = newTxBuf; - port->txSize = (uint16_t)txSz; - port->txHead = 0; - port->txTail = 0; - port->txCount = 0; - port->comDeb.qInSize = (uint16_t)rxSz; - port->comDeb.qInCount = 0; - port->comDeb.qInHead = 0; - port->comDeb.qInTail = 0; - port->comDeb.qOutSize = (uint16_t)txSz; - port->comDeb.qOutCount = 0; - port->comDeb.qOutHead = 0; - port->comDeb.qOutTail = 0; - _enable(); - - // Free old buffers AFTER re-enabling interrupts - if (oldRxH) { - GlobalUnlock(oldRxH); - GlobalFree(oldRxH); - } - if (oldTxH) { - GlobalUnlock(oldTxH); - GlobalFree(oldTxH); - } - } - - dbgStr("KPCOMM: setque OK\r\n"); - return 0; + return (writeTx(port, &data, 1) == 1) ? 0 : SNDCOM_FAILED; } // ----------------------------------------------------------------------- -// sndcom - Write to TX ring buffer and prime transmitter (ordinal 5) +// stacom - Return and clear the error flags; fill COMSTAT (ordinal 8) // -// Copies up to len bytes from buf into the transmit ring buffer. -// Returns number of bytes written, or negative error code. +// Returns CE_MODE for an invalid cid. // ----------------------------------------------------------------------- -int16_t FAR PASCAL _export sndcom(int16_t commId, void FAR *buf, int16_t len) { - PortStateT *port; - uint8_t FAR *src; - int16_t bytesWritten; - - dbgInt16("KPCOMM: sndcom Id", commId); - - if (commId < 0 || commId >= MAX_PORTS) { - return -1; - } - - port = &ports[commId]; - if (!port->isOpen) { - return -1; - } - - src = (uint8_t FAR *)buf; - bytesWritten = 0; - - _disable(); - while (bytesWritten < len) { - if (port->txCount >= port->txSize) { - port->errorFlags |= CE_TXFULL; - break; - } - - port->txBuf[port->txHead] = *src++; - port->txHead++; - if (port->txHead >= port->txSize) { - port->txHead = 0; - } - port->txCount++; - bytesWritten++; - } - _enable(); - - // Sync ComDEB queue counts - port->comDeb.qOutCount = port->txCount; - port->comDeb.qOutHead = port->txHead; - - // Prime transmitter if we wrote anything - if (bytesWritten > 0 && !port->txStopped) { - primeTx(port); - } - - return bytesWritten; -} - - -// ----------------------------------------------------------------------- -// stacom - Return and clear error flags plus COMSTAT (ordinal 8) -// -// Returns accumulated error flags (CE_*) and fills the COMSTAT -// structure with current buffer counts and status. -// ----------------------------------------------------------------------- -int16_t FAR PASCAL _export stacom(int16_t commId, COMSTAT FAR *stat) { +uint16_t FAR PASCAL _export stacom(int16_t commId, COMSTAT FAR *stat) { PortStateT *port; - int16_t errors; + LptStateT *lpt; + uint16_t errors; + uint16_t inQue; + uint16_t outQue; + uint8_t status; - dbgInt16("KPCOMM: stacom Id", commId); - - if (commId < 0 || commId >= MAX_PORTS) { - dbgStr("KPCOMM: stacom bad id\r\n"); - return -1; + lpt = getOpenLpt(commId); + if (lpt) { + errors = lpt->errorFlags; + lpt->errorFlags = 0; + if (stat) { + _fmemset(stat, 0, sizeof(COMSTAT)); + } + return errors; } - port = &ports[commId]; - if (!port->isOpen) { - dbgStr("KPCOMM: stacom not open\r\n"); - return -1; + port = getOpenPort(commId); + if (!port) { + return CE_MODE; } + status = 0; + _disable(); - errors = (int16_t)port->errorFlags; + errors = port->errorFlags; port->errorFlags = 0; + if (port->txHold & TX_HOLD_CTS) { + status |= CSTF_CTSHOLD; + } + if (port->txHold & TX_HOLD_DSR) { + status |= CSTF_DSRHOLD; + } + if (port->txHold & TX_HOLD_RLSD) { + status |= CSTF_RLSDHOLD; + } + if (port->txHold & TX_HOLD_XOFF) { + status |= CSTF_XOFFHOLD; + } + if (port->rxThrottle & RX_THROTTLE_XOFF) { + status |= CSTF_XOFFSENT; + } + if (port->eofSeen) { + status |= CSTF_EOF; + } + if (port->txImmediate != NO_CHAR) { + status |= CSTF_TXIM; + } + inQue = port->comDeb.rxCount; + outQue = port->comDeb.txCount; _enable(); if (stat) { - stat->status = 0; - if (port->txStopped) { - stat->status |= CSTF_XOFFHOLD; - } - stat->cbInQue = port->rxCount; - stat->cbOutQue = port->txCount; + stat->status = status; + stat->cbInQue = inQue; + stat->cbOutQue = outQue; } - - dbgHex16("KPCOMM: stacom errors", (uint16_t)errors); return errors; } // ----------------------------------------------------------------------- -// suspendOpenCommPorts - Suspend all ports during task switch (ordinal 17) +// suspendOpenCommPorts - Hand the hardware to a DOS application +// (ordinal 17) // -// Called by Windows when switching away from this VM. -// Disables interrupts on all open ports to prevent ISR problems. +// Called by WINOLDAP. Our UARTs are silenced, and the ISR passes every +// interrupt on our IRQs to the previous handlers until reactivation. DTR +// and RTS are left alone so a modem connection survives the switch. // ----------------------------------------------------------------------- void FAR PASCAL _export suspendOpenCommPorts(void) { - int16_t i; - PortStateT *port; + int16_t i; dbgStr("KPCOMM: suspend\r\n"); for (i = 0; i < MAX_PORTS; i++) { - port = &ports[i]; - if (!port->isOpen) { - continue; + if (ports[i].isOpen) { + _disable(); + writeIer(&ports[i], 0); + _enable(); + } + } + portsSuspended = true; +} + + +// ----------------------------------------------------------------------- +// triggerCode - FCR trigger code (0-3) for a trigger level in bytes, or -1 +// ----------------------------------------------------------------------- +static int16_t triggerCode(uint16_t bytes) { + int16_t code; + + for (code = 0; code < (int16_t)sizeof(fifoTriggerBytes); code++) { + if (fifoTriggerBytes[code] == bytes) { + return code; + } + } + return -1; +} + + +// ----------------------------------------------------------------------- +// trmcom - Close a port (ordinal 7) +// +// Transmits the output queue first (DDAG). DTR is dropped unless the port +// was told to keep it (COMnKeepDTR / KPESC_KEEP_DTR_ON_CLOSE). Returns 0, +// TRMCOM_TIMEOUT if the output could not be sent, or 0x8000 (CE_MODE) for +// an invalid cid. +// ----------------------------------------------------------------------- +int16_t FAR PASCAL _export trmcom(int16_t commId) { + PortStateT *port; + bool drained; + + dbgHex16("KPCOMM: trmcom id", (uint16_t)commId); + + if (isLptId(commId)) { + return lptClose(commId); + } + + port = getOpenPort(commId); + if (!port) { + return (int16_t)CE_MODE; + } + + drained = drainTx(port); + closePort(port); + return drained ? 0 : TRMCOM_TIMEOUT; +} + + +// ----------------------------------------------------------------------- +// uartPresent - True if a UART answers at base +// +// LCR reads back what was written; an empty I/O address floats to 0xFF. +// ----------------------------------------------------------------------- +static bool uartPresent(uint16_t base) { + _outp(base + UART_LCR, UART_PRESENT_LCR); + return (uint8_t)_inp(base + UART_LCR) == UART_PRESENT_LCR; +} + + +// ----------------------------------------------------------------------- +// waitTxLines - Wait for the modem lines that finite DCB timeouts guard +// +// Cts/Dsr/RlsTimeout: IGNORE (0) skips the line, INFINITE makes it flow +// control (the ISR holds output through txHold), and anything else is how +// many milliseconds output waits for the line to go high (DDAG). Called +// before output is accepted, with interrupts enabled, watching the MSR +// shadow the ISR keeps current. The wait blocks the caller, as the stock +// driver's did. Returns the CE_CTSTO/CE_DSRTO/CE_RLSDTO bits of lines +// still low when their time ran out (0 = go ahead). +// ----------------------------------------------------------------------- +static uint16_t waitTxLines(PortStateT *port) { + uint16_t elapsed; + uint16_t errors; + uint16_t timeout; + uint16_t lastTick; + uint16_t tick; + uint16_t i; + uint8_t msr; + bool waiting; + + // Without the BIOS tick there is no clock: time out at once + elapsed = (biosDataSelector() != 0) ? 0 : UINT16_MAX; + lastTick = readBiosWord(BDA_TICK_COUNT); + + for (;;) { + msr = port->comDeb.msrShadow; + errors = 0; + waiting = false; + + for (i = 0; i < TX_LINE_COUNT; i++) { + timeout = *(const uint16_t *)((const uint8_t *)&port->dcb + txLines[i].timeoutOffset); + if (timeout == IGNORE || timeout == INFINITE || (msr & txLines[i].msrBit)) { + continue; + } + if (elapsed >= timeout) { + errors |= txLines[i].error; + } else { + waiting = true; + } } - // Disable UART interrupts - _outp(port->baseAddr + UART_IER, 0); + if (!waiting) { + return errors; + } - // Mask IRQ at PIC - { - uint8_t picMask = (uint8_t)_inp(PIC_DATA); - picMask |= port->irqMask; - _outp(PIC_DATA, picMask); + tick = readBiosWord(BDA_TICK_COUNT); + if (tick != lastTick) { + lastTick = tick; + elapsed = (elapsed < UINT16_MAX - MS_PER_BIOS_TICK) ? (uint16_t)(elapsed + MS_PER_BIOS_TICK) : UINT16_MAX; } } } // ----------------------------------------------------------------------- -// trmcom - Close a COM port (ordinal 7) +// writeLine - Program the divisor and LCR, keeping the break state // -// Disables interrupts, drops DTR/RTS, unhooks ISR, frees buffers. -// Returns 0 on success. +// Interrupts must be disabled: while DLAB is set, offsets 0/1 are the +// divisor latch and the ISR would read garbage from "RBR". // ----------------------------------------------------------------------- -int16_t FAR PASCAL _export trmcom(int16_t commId) { - PortStateT *port; +static void writeLine(PortStateT *port, uint16_t divisor, uint8_t lcr) { + uint16_t base; - dbgInt16("KPCOMM: trmcom Id", commId); + base = port->comDeb.baseAddr; + lcr = (uint8_t)((lcr & ~LCR_SBRK) | (port->comDeb.lcrShadow & LCR_SBRK)); - if (commId < 0 || commId >= MAX_PORTS) { - return -1; + _outp(base + UART_LCR, lcr | LCR_DLAB); + _outp(base + UART_DLL, LOBYTE(divisor)); + _outp(base + UART_DLM, HIBYTE(divisor)); + _outp(base + UART_LCR, lcr); + + port->comDeb.lcrShadow = lcr; +} + + +// ----------------------------------------------------------------------- +// writeTx - Copy up to len bytes into the transmit queue and start TX +// +// First waits for any modem lines a finite DCB timeout guards; if one +// stays low, nothing is queued and its CE_*TO bit is set. Single +// producer: only this side moves txHead, and the ISR only shrinks txCount, +// so the free space seen here can only grow. The copy runs with +// interrupts enabled; publishing the new count is the only guarded step. +// Sets CE_TXFULL if not everything fit. Returns the bytes queued. +// ----------------------------------------------------------------------- +static uint16_t writeTx(PortStateT *port, const uint8_t FAR *src, uint16_t len) { + uint8_t FAR *buf; + uint16_t size; + uint16_t head; + uint16_t room; + uint16_t chunk; + uint16_t lineErrors; + + lineErrors = waitTxLines(port); + if (lineErrors) { + addErrors(port, lineErrors); + return 0; } - port = &ports[commId]; - if (!port->isOpen) { - dbgStr("KPCOMM: trmcom port not open\r\n"); - return -1; + size = port->comDeb.txSize; + room = size - port->comDeb.txCount; + if (room > len) { + room = len; } - // Disable UART interrupts - _outp(port->baseAddr + UART_IER, 0); - - // Reset FIFOs to drain pending data, then disable - if (port->is16550) { - _outp(port->baseAddr + UART_FCR, FCR_ENABLE | FCR_RX_RESET | FCR_TX_RESET); - _outp(port->baseAddr + UART_FCR, 0); + if (room > 0) { + buf = port->txBuf.data; + head = port->comDeb.txHead; + chunk = size - head; + if (chunk > room) { + chunk = room; + } + _fmemcpy(buf + head, src, chunk); + if (chunk < room) { + _fmemcpy(buf, src + chunk, room - chunk); + head = room - chunk; + } else { + // head + chunk <= size, so this cannot overflow 16 bits + head += chunk; + if (head == size) { + head = 0; + } + } + port->comDeb.txHead = head; } - // Drop DTR, RTS, OUT2 - _outp(port->baseAddr + UART_MCR, 0); + _disable(); + port->comDeb.txCount += room; + if (room < len) { + port->errorFlags |= CE_TXFULL; + } + startTx(port); + refreshNotify(port, 0); + _enable(); - // Drain all pending UART conditions so the hardware is clean - // for the next open. Each read clears the corresponding - // interrupt source inside the UART. - _inp(port->baseAddr + UART_LSR); - _inp(port->baseAddr + UART_MSR); - _inp(port->baseAddr + UART_RBR); - _inp(port->baseAddr + UART_IIR); - - // Unhook ISR - unhookIsr(port); - - // Free buffers - freeBuffers(port); - - // Clear notification - port->hwndNotify = NULL; - - port->isOpen = FALSE; - - dbgHex16("KPCOMM: trmcom isrHits", isrHitCount); - dbgStr("KPCOMM: trmcom OK\r\n"); - return 0; + return room; } // ----------------------------------------------------------------------- // LibMain - DLL entry point +// +// Static data is zero-filled by the loader, which is the closed state for +// every port. // ----------------------------------------------------------------------- int FAR PASCAL LibMain(HANDLE hInstance, WORD wDataSeg, WORD wHeapSize, LPSTR lpCmdLine) { - int16_t i; - + (void)hInstance; (void)wDataSeg; + (void)wHeapSize; (void)lpCmdLine; - ghInstance = hInstance; - dbgStr("KPCOMM: LibMain\r\n"); - - if (wHeapSize > 0) { - UnlockData(0); - } - - // Zero all port states - for (i = 0; i < MAX_PORTS; i++) { - _fmemset(&ports[i], 0, sizeof(PortStateT)); - ports[i].txImmediate = -1; - } - return 1; } // ----------------------------------------------------------------------- // WEP - DLL exit procedure +// +// Releases hardware without waiting for output: Windows is going away. +// Ports set to keep DTR leave it up, so a modem connection can outlive +// Windows itself. // ----------------------------------------------------------------------- int FAR PASCAL _export WEP(int nParam) { int16_t i; (void)nParam; - // Close any ports still open for (i = 0; i < MAX_PORTS; i++) { if (ports[i].isOpen) { - trmcom(i); + closePort(&ports[i]); } } + lptCloseAll(); return 1; } diff --git a/drv/commdrv.h b/drv/commdrv.h index 0f0eb2a..83a82ba 100644 --- a/drv/commdrv.h +++ b/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 #include +#include +#include + +// 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 diff --git a/drv/isr.c b/drv/isr.c index 628cb79..5c65be9 100644 --- a/drv/isr.c +++ b/drv/isr.c @@ -1,483 +1,977 @@ -// isr.c - Interrupt service routines for KPCOMM.DRV +// isr.c - Interrupt-time code for KPCOMM.DRV // -// ISR entry points for IRQ3 (COM2/4) and IRQ4 (COM1/3), DPMI interrupt -// vector hooking/unhooking, and interrupt dispatch with 16550 FIFO support. +// Hardware interrupt entry points, DPMI vector hooking with a per-IRQ +// table (shared IRQs, slave PIC, chaining), UART interrupt dispatch, and +// the transmit, flow-control, and notification helpers that the +// application-context exports share with the ISR. #include "commdrv.h" -#include + +#define MAX_IIR_LOOPS 64 // Bound on IIR reads per port per interrupt (stuck hardware guard) +#define MAX_IRQ_PASSES 32 // Bound on full passes over ports sharing one IRQ +#define MAX_RX_BURST (FIFO_DEPTH * 4) // Bound on bytes drained per RX interrupt source + +typedef void (_far _interrupt *IsrProcT)(void); + +// Per-IRQ hook state +typedef struct { + IsrProcT handler; // Our entry point for this IRQ (NULL = unsupported) + IsrProcT prevIsr; // Vector installed before ours + int16_t refCount; // Open ports using this IRQ + bool wasMasked; // PIC mask bit before our first hook +} IrqHookT; // ----------------------------------------------------------------------- -// Prototypes +// Prototypes (public functions are declared in commdrv.h) // ----------------------------------------------------------------------- -static void checkFlowRx(PortStateT *port); -static void handleLsr(PortStateT *port, uint8_t lsr); -static void handleMsr(PortStateT *port); -static void handleRx(PortStateT *port, uint8_t lsr); -static void handleTx(PortStateT *port); - -void _far _interrupt isr3(void); -void _far _interrupt isr4(void); +static void chainIsr(IsrProcT prevIsr); +static void eoiIrq(uint8_t irq); +static bool getPmVector(uint8_t intNum, IsrProcT FAR *isr); +static void handleLsr(PortStateT *port, uint8_t lsr); +static void handleMsr(PortStateT *port); +static void handleRx(PortStateT *port, uint8_t lsr); +static void handleTx(PortStateT *port); +static bool irqInService(uint8_t irq); +static void _far _interrupt isrIrq03(void); +static void _far _interrupt isrIrq04(void); +static void _far _interrupt isrIrq05(void); +static void _far _interrupt isrIrq07(void); +static void _far _interrupt isrIrq09(void); +static void _far _interrupt isrIrq10(void); +static void _far _interrupt isrIrq11(void); +static void _far _interrupt isrIrq12(void); +static void _far _interrupt isrIrq15(void); +static bool maskIrq(uint8_t irq, bool masked); +static uint8_t pmVector(uint8_t irq); +static void raiseEvents(PortStateT *port, uint16_t events); +static void serviceIrq(uint8_t irq); +static bool servicePort(PortStateT *port); +static bool setPmVector(uint8_t intNum, IsrProcT isr); +static bool txSendable(PortStateT *port); // ----------------------------------------------------------------------- -// Saved previous ISR vectors for IRQ3 and IRQ4 +// USER's PostMessage, resolved by enableNotification (COMM.DRV loads +// before USER, so it cannot be imported statically). PostMessage is one +// of the functions Windows 3.1 allows at interrupt time. +// ----------------------------------------------------------------------- +PostMessageProcT postMessageProc = NULL; + +// ----------------------------------------------------------------------- +// IRQ hook table. IRQs with no handler belong to system devices. An +// ISA card jumpered to IRQ 2 interrupts on IRQ 9 (see readPortConfig). +// ----------------------------------------------------------------------- +static IrqHookT irqHooks[IRQ_COUNT] = { + {NULL, NULL, 0, false}, // 0: system timer + {NULL, NULL, 0, false}, // 1: keyboard + {NULL, NULL, 0, false}, // 2: cascade from slave PIC + {isrIrq03, NULL, 0, false}, // 3: COM2/COM4 + {isrIrq04, NULL, 0, false}, // 4: COM1/COM3 + {isrIrq05, NULL, 0, false}, // 5 + {NULL, NULL, 0, false}, // 6: floppy + {isrIrq07, NULL, 0, false}, // 7 + {NULL, NULL, 0, false}, // 8: real-time clock + {isrIrq09, NULL, 0, false}, // 9: ISA IRQ 2 + {isrIrq10, NULL, 0, false}, // 10 + {isrIrq11, NULL, 0, false}, // 11 + {isrIrq12, NULL, 0, false}, // 12 + {NULL, NULL, 0, false}, // 13: coprocessor + {NULL, NULL, 0, false}, // 14: primary disk + {isrIrq15, NULL, 0, false} // 15 +}; + + +// ----------------------------------------------------------------------- +// chainIsr - Pass the current interrupt to the handler we replaced // -// When we hook an IRQ via DPMI, the original vector is saved here so -// unhookIsr can restore it. Only saved/restored on first-use/last-use -// of each IRQ (ref-counted for shared IRQs like COM1+COM3). +// Simulates an interrupt (PUSHF + far CALL) so the previous handler's +// IRET returns here. The previous handler sends its own EOI. // ----------------------------------------------------------------------- -static void (_far _interrupt *prevIsr3)(void) = NULL; // Original IRQ3 vector -static void (_far _interrupt *prevIsr4)(void) = NULL; // Original IRQ4 vector - -// Reference counts: how many open ports share each IRQ. -// hookIsr increments; unhookIsr decrements. Vector is only -// installed/restored when the count transitions through 0. -static int16_t irq3RefCount = 0; -static int16_t irq4RefCount = 0; +static void chainIsr(IsrProcT prevIsr) { + _asm { + pushf + call dword ptr prevIsr + } +} // ----------------------------------------------------------------------- -// checkFlowRx - Check RX buffer level and assert/deassert flow control +// checkNotify - Post WM_COMMNOTIFY for conditions that have become true +// +// Each notification is edge-triggered: once posted it stays quiet until +// refreshNotify re-arms it. CN_RECEIVE: the DDAG allows fewer than +// wInTrigger bytes once an internal timeout expires; KPCOMM has no timer, +// so the timeout is zero and any waiting data qualifies. A failed post +// (application queue full) is not recorded, so the next call retries. +// Interrupts disabled. // ----------------------------------------------------------------------- -static void checkFlowRx(PortStateT *port) { - uint16_t base; +void checkNotify(PortStateT *port) { + uint16_t bits; - base = port->baseAddr; - - if (port->hsMode == HS_NONE) { + if (!port->hwndNotify || !postMessageProc) { return; } - // Check if we need to stop remote sender - if (!port->rxStopped && port->rxCount >= (port->rxSize - port->xoffLim)) { - port->rxStopped = TRUE; + bits = 0; - if (port->hsMode == HS_XONXOFF || port->hsMode == HS_BOTH) { - // Send XOFF - queue as immediate character - port->txImmediate = port->xoffChar; - // Enable THRE to get it sent - _outp(base + UART_IER, (uint8_t)(_inp(base + UART_IER) | IER_THRE)); - } - if (port->hsMode == HS_RTSCTS || port->hsMode == HS_BOTH) { - // Drop RTS - _outp(base + UART_MCR, (uint8_t)(_inp(base + UART_MCR) & ~MCR_RTS)); + if (port->rxNotifyTrigger != NOTIFY_OFF && !(port->notifySent & CN_RECEIVE) && port->comDeb.rxCount > 0) { + bits |= CN_RECEIVE; + } + + if (port->txNotifyTrigger != NOTIFY_OFF && !(port->notifySent & CN_TRANSMIT)) { + if (port->comDeb.txCount < (uint16_t)port->txNotifyTrigger) { + bits |= CN_TRANSMIT; } } - // Check if we can resume remote sender - if (port->rxStopped && port->rxCount <= port->xonLim) { - port->rxStopped = FALSE; + if (!(port->notifySent & CN_EVENT) && (port->comDeb.evtWord & port->comDeb.evtMask)) { + bits |= CN_EVENT; + } - if (port->hsMode == HS_XONXOFF || port->hsMode == HS_BOTH) { - // Send XON - port->txImmediate = port->xonChar; - _outp(base + UART_IER, (uint8_t)(_inp(base + UART_IER) | IER_THRE)); - } - if (port->hsMode == HS_RTSCTS || port->hsMode == HS_BOTH) { - // Raise RTS - _outp(base + UART_MCR, (uint8_t)(_inp(base + UART_MCR) | MCR_RTS)); - } + if (bits && postMessageProc(port->hwndNotify, WM_COMMNOTIFY, (WPARAM)(port - ports), MAKELONG(bits, 0))) { + port->notifySent = (uint8_t)(port->notifySent | bits); } } // ----------------------------------------------------------------------- -// handleLsr - Process line status errors +// eoiIrq - Acknowledge an IRQ at the PIC(s) +// ----------------------------------------------------------------------- +static void eoiIrq(uint8_t irq) { + if (irq >= IRQ_SLAVE_FIRST) { + _outp(PIC2_CMD, PIC_EOI); + } + _outp(PIC1_CMD, PIC_EOI); +} + + +// ----------------------------------------------------------------------- +// getPmVector - Read a protected-mode interrupt vector (DPMI 0204h) +// ----------------------------------------------------------------------- +static bool getPmVector(uint8_t intNum, IsrProcT FAR *isr) { + uint16_t sel; + uint16_t off; + int16_t failed; + + _asm { + mov ax, DPMI_GET_PM_VECTOR + mov bl, intNum + int DPMI_INT + mov sel, cx + mov off, dx + sbb ax, ax + mov failed, ax + } + + if (failed) { + return false; + } + *isr = (IsrProcT)MAKELP(sel, off); + return true; +} + + +// ----------------------------------------------------------------------- +// handleLsr - Record line status errors and their events // ----------------------------------------------------------------------- static void handleLsr(PortStateT *port, uint8_t lsr) { + uint16_t errors; + uint16_t events; + + errors = 0; + events = 0; + if (lsr & LSR_OE) { - port->errorFlags |= CE_OVERRUN; + errors |= CE_OVERRUN; } if (lsr & LSR_PE) { - port->errorFlags |= CE_RXPARITY; + errors |= CE_RXPARITY; } if (lsr & LSR_FE) { - port->errorFlags |= CE_FRAME; - } - if (lsr & LSR_BI) { - port->errorFlags |= CE_BREAK; - port->comDeb.evtWord |= EV_BREAK; + errors |= CE_FRAME; } if (lsr & (LSR_OE | LSR_PE | LSR_FE)) { - port->comDeb.evtWord |= EV_ERR; + events |= EV_ERR; } + if (lsr & LSR_BI) { + errors |= CE_BREAK; + events |= EV_BREAK; + } + + port->errorFlags |= errors; + raiseEvents(port, events); } // ----------------------------------------------------------------------- -// handleMsr - Process modem status changes +// handleMsr - Record modem line changes and re-evaluate TX holds // ----------------------------------------------------------------------- static void handleMsr(PortStateT *port) { - uint8_t msr; + uint16_t events; + uint8_t msr; - msr = (uint8_t)_inp(port->baseAddr + UART_MSR); - - // Update ComDEB MSR shadow (offset 35) for third-party code + msr = (uint8_t)_inp(port->comDeb.baseAddr + UART_MSR); port->comDeb.msrShadow = msr; + events = 0; if (msr & MSR_DCTS) { - port->comDeb.evtWord |= EV_CTS; - - // RTS/CTS flow control: stop/start TX based on CTS - if (port->hsMode == HS_RTSCTS || port->hsMode == HS_BOTH) { - if (msr & MSR_CTS) { - port->txStopped = FALSE; - // Resume TX - if (port->txCount > 0) { - handleTx(port); - } - } else { - port->txStopped = TRUE; - } - } + events |= EV_CTS; } - if (msr & MSR_DDSR) { - port->comDeb.evtWord |= EV_DSR; + events |= EV_DSR; } - if (msr & MSR_TERI) { - port->comDeb.evtWord |= EV_RING; + events |= EV_RING; } - if (msr & MSR_DDCD) { - port->comDeb.evtWord |= EV_RLSD; + events |= EV_RLSD; } + raiseEvents(port, events); + + updateTxHold(port); } // ----------------------------------------------------------------------- -// handleRx - Read all available bytes from UART into RX ring buffer +// handleRx - Drain the UART into the RX ring buffer +// +// Applies the DCB receive options per byte: parity replacement (fParity + +// fPeChar), XON/XOFF recognition (fOutX), null stripping (fNull), the +// event character (fChEvt), and EOF detection in non-binary mode. +// rxHead/rxCount are cached locally: the application only changes them +// with interrupts disabled, so they cannot move under the ISR. The loop +// is bounded and stops on an all-ones LSR (a vanished UART reads 0xFF, +// which looks like endless data); leftovers keep IIR pending for the next +// servicePort pass. // ----------------------------------------------------------------------- static void handleRx(PortStateT *port, uint8_t lsr) { - uint16_t base; - uint8_t ch; + DCB *dcb; + uint8_t FAR *buf; + uint16_t base; + uint16_t size; + uint16_t count; + uint16_t head; + uint16_t events; + uint16_t burst; + uint8_t ch; - base = port->baseAddr; + dcb = &port->dcb; + buf = port->rxBuf.data; + base = port->comDeb.baseAddr; + size = port->comDeb.rxSize; + count = port->comDeb.rxCount; + head = port->comDeb.rxHead; + events = 0; - while (lsr & LSR_DR) { - // Check for line errors on this byte - if (lsr & (LSR_OE | LSR_PE | LSR_FE | LSR_BI)) { + for (burst = 0; burst < MAX_RX_BURST && (lsr & LSR_DR) && lsr != UART_ABSENT; burst++) { + if (lsr & LSR_ERRORS) { handleLsr(port, lsr); } ch = (uint8_t)_inp(base + UART_RBR); - // Check for XON/XOFF if software flow control enabled - if (port->hsMode == HS_XONXOFF || port->hsMode == HS_BOTH) { - if (ch == port->xonChar) { - port->txStopped = FALSE; - // Resume TX if we have data - if (port->txCount > 0) { - handleTx(port); + if ((lsr & LSR_PE) && dcb->fParity && dcb->fPeChar) { + ch = (uint8_t)dcb->PeChar; + } + + if (dcb->fOutX && ch == (uint8_t)dcb->XonChar) { + port->txHold = (uint8_t)(port->txHold & ~TX_HOLD_XOFF); + startTx(port); + } else if (dcb->fOutX && ch == (uint8_t)dcb->XoffChar) { + port->txHold = (uint8_t)(port->txHold | TX_HOLD_XOFF); + } else if (!(dcb->fNull && ch == 0)) { + if (dcb->fChEvt && ch == (uint8_t)dcb->EvtChar) { + events |= EV_RXFLAG; + } + if (!dcb->fBinary && ch == (uint8_t)dcb->EofChar) { + port->eofSeen = true; + } + if (count < size) { + buf[head] = ch; + head++; + if (head >= size) { + head = 0; } - lsr = (uint8_t)_inp(base + UART_LSR); - continue; - } - if (ch == port->xoffChar) { - port->txStopped = TRUE; - lsr = (uint8_t)_inp(base + UART_LSR); - continue; + count++; + events |= EV_RXCHAR; + } else { + port->errorFlags |= CE_RXOVER; } } - // Store in ring buffer - if (port->rxCount < port->rxSize) { - port->rxBuf[port->rxHead] = ch; - port->rxHead++; - if (port->rxHead >= port->rxSize) { - port->rxHead = 0; - } - port->rxCount++; - } else { - // Buffer overflow - port->errorFlags |= CE_RXOVER; - } - - // Set event bit - port->comDeb.evtWord |= EV_RXCHAR; - - // Read LSR again for next byte lsr = (uint8_t)_inp(base + UART_LSR); } - // Check if we need to assert flow control - checkFlowRx(port); + port->comDeb.rxHead = head; + port->comDeb.rxCount = count; + raiseEvents(port, events); + updateRxFlow(port); } // ----------------------------------------------------------------------- -// handleTx - Transmit bytes from TX ring buffer to UART +// handleTx - Refill the transmitter on a THRE interrupt // -// For 16550: burst up to 16 bytes per THRE interrupt. -// For 8250: send 1 byte at a time. +// THRE means the holding register (and FIFO) is empty, so a full burst +// is safe here -- and only here; every other path goes through startTx. +// Order: our XON/XOFF (ignores the remote's XOFF, respects CTS/DSR), the +// ctx() character, then queued data. When nothing may be sent the THRE +// interrupt is disabled, so the next startTx is a clean 0 -> 1 enable. // ----------------------------------------------------------------------- static void handleTx(PortStateT *port) { - uint16_t base; - uint16_t burst; - uint16_t i; + uint8_t FAR *buf; + uint16_t base; + uint16_t room; + uint16_t sent; + uint16_t burst; + uint16_t tail; + uint16_t size; + uint16_t i; - if (port->txStopped) { - return; + base = port->comDeb.baseAddr; + room = port->useFifo ? FIFO_DEPTH : 1; + sent = 0; + + if (port->flowChar != NO_CHAR && !(port->txHold & TX_HOLD_HW)) { + _outp(base + UART_THR, (uint8_t)port->flowChar); + port->flowChar = NO_CHAR; + sent++; } - base = port->baseAddr; - - // Priority character first - if (port->txImmediate >= 0) { - _outp(base + UART_THR, (uint8_t)port->txImmediate); - port->txImmediate = -1; - return; - } - - if (port->txCount == 0) { - // Nothing to send -- disable THRE interrupt - _outp(base + UART_IER, (uint8_t)(_inp(base + UART_IER) & ~IER_THRE)); - // TX empty event - port->comDeb.evtWord |= EV_TXEMPTY; - return; - } - - // Burst size: 16 for FIFO, 1 for non-FIFO or FIFO disabled - burst = (port->is16550 && port->fifoEnabled) ? FIFO_DEPTH : 1; - if (burst > port->txCount) { - burst = port->txCount; - } - - for (i = 0; i < burst; i++) { - _outp(base + UART_THR, port->txBuf[port->txTail]); - port->txTail++; - if (port->txTail >= port->txSize) { - port->txTail = 0; + if (port->txHold == 0) { + if (sent < room && port->txImmediate != NO_CHAR) { + _outp(base + UART_THR, (uint8_t)port->txImmediate); + port->txImmediate = NO_CHAR; + sent++; + } + + burst = port->comDeb.txCount; + if (burst > room - sent) { + burst = room - sent; + } + if (burst > 0) { + buf = port->txBuf.data; + tail = port->comDeb.txTail; + size = port->comDeb.txSize; + for (i = 0; i < burst; i++) { + _outp(base + UART_THR, buf[tail]); + tail++; + if (tail >= size) { + tail = 0; + } + } + port->comDeb.txTail = tail; + port->comDeb.txCount -= burst; + sent += burst; + } + } + + if (sent == 0) { + writeIer(port, (uint8_t)(port->comDeb.ierShadow & ~IER_THRE)); + if (port->comDeb.txCount == 0) { + raiseEvents(port, EV_TXEMPTY); } - port->txCount--; } } // ----------------------------------------------------------------------- -// hookIsr - Hook the interrupt vector for a port's IRQ via DPMI +// hookIsr - Route a port's IRQ to our handler // -// Uses INT 31h AX=0204h to get and AX=0205h to set protected-mode -// interrupt vectors. -// -// Returns 0 on success, -1 on error. +// The vector is hooked (DPMI 0205h) and the IRQ unmasked only for the +// first port on an IRQ. The prior mask state is recorded so unhookIsr +// can restore it and so serviceIrq knows whether another device may +// share the line. The line is held masked while the hook state is +// published, so the ISR never sees a stale prevIsr or wasMasked; an edge +// in that window stays latched in the PIC. Returns 0, or DRV_ERROR for +// an unsupported IRQ or a DPMI failure. // ----------------------------------------------------------------------- int16_t hookIsr(PortStateT *port) { - uint8_t intNum; - uint8_t picMask; - int16_t *refCount; - void (_far _interrupt **prevPtr)(void); - void (_far _interrupt *newIsr)(void); - void _far *oldVector; - uint16_t oldSeg; - uint16_t oldOff; + IrqHookT *hook; + IsrProcT prevIsr; + uint8_t intNum; - intNum = port->irq + 8; - port->irqMask = (uint8_t)(1 << port->irq); - - if (port->irq == 3) { - prevPtr = &prevIsr3; - refCount = &irq3RefCount; - newIsr = isr3; - } else { - prevPtr = &prevIsr4; - refCount = &irq4RefCount; - newIsr = isr4; + if (port->irq >= IRQ_COUNT || !irqHooks[port->irq].handler) { + return DRV_ERROR; } - // Only hook the vector on first use of this IRQ - if (*refCount == 0) { - // INT 31h AX=0204h: Get Protected Mode Interrupt Vector - // BL = interrupt number - // Returns: CX:DX = selector:offset of handler - _asm { - mov ax, 0204h - mov bl, intNum - int 31h - mov oldSeg, cx - mov oldOff, dx + hook = &irqHooks[port->irq]; + + if (hook->refCount == 0) { + intNum = pmVector(port->irq); + if (!getPmVector(intNum, &prevIsr)) { + return DRV_ERROR; } - oldVector = (void _far *)((uint32_t)oldSeg << 16 | oldOff); - *prevPtr = (void (_far _interrupt *)(void))oldVector; - - // INT 31h AX=0205h: Set Protected Mode Interrupt Vector - // BL = interrupt number - // CX:DX = selector:offset of new handler - { - uint16_t newSeg = _FP_SEG(newIsr); - uint16_t newOff = _FP_OFF(newIsr); - - _asm { - mov ax, 0205h - mov bl, intNum - mov cx, newSeg - mov dx, newOff - int 31h + hook->wasMasked = maskIrq(port->irq, true); + hook->prevIsr = prevIsr; + if (!setPmVector(intNum, hook->handler)) { + hook->prevIsr = NULL; + if (!hook->wasMasked) { + maskIrq(port->irq, false); } + return DRV_ERROR; } + maskIrq(port->irq, false); } - (*refCount)++; - port->prevIsr = (void (FAR *)(void))*prevPtr; - - // Unmask IRQ at PIC - picMask = _inp(PIC_DATA); - picMask &= ~port->irqMask; - _outp(PIC_DATA, picMask); - + hook->refCount++; return 0; } // ----------------------------------------------------------------------- -// isrDispatch - Main interrupt dispatch loop +// irqInService - True if the PIC has an IRQ in service (OCW3 ISR read) // -// Called from the ISR entry points with a pointer to the port state. -// Reads IIR in a loop until no more interrupts are pending. -// Handles in priority order: LSR > RX > TX > MSR. +// Restores the default IRR read mode afterwards. Interrupts disabled. // ----------------------------------------------------------------------- -void isrDispatch(PortStateT *port) { +static bool irqInService(uint8_t irq) { + uint16_t cmdPort; + uint8_t inService; + + cmdPort = (irq >= IRQ_SLAVE_FIRST) ? PIC2_CMD : PIC1_CMD; + + _outp(cmdPort, PIC_READ_ISR); + inService = (uint8_t)_inp(cmdPort); + _outp(cmdPort, PIC_READ_IRR); + + return (inService & (1 << (irq % IRQ_SLAVE_FIRST))) != 0; +} + + +// ----------------------------------------------------------------------- +// isrIrq03 - Hardware interrupt entry point for IRQ 3 +// +// _interrupt saves and restores DS but does not load it, so point DS at +// the driver's data segment before touching any global. CLD protects +// string instructions from a direction flag left set by interrupted code. +// The other isrIrqNN entry points are identical apart from the IRQ. +// ----------------------------------------------------------------------- +static void _far _interrupt isrIrq03(void) { + _asm mov ax, seg ports + _asm mov ds, ax + _asm cld + serviceIrq(3); +} + + +// ----------------------------------------------------------------------- +// isrIrq04 - Hardware interrupt entry point for IRQ 4 (see isrIrq03) +// ----------------------------------------------------------------------- +static void _far _interrupt isrIrq04(void) { + _asm mov ax, seg ports + _asm mov ds, ax + _asm cld + serviceIrq(4); +} + + +// ----------------------------------------------------------------------- +// isrIrq05 - Hardware interrupt entry point for IRQ 5 (see isrIrq03) +// ----------------------------------------------------------------------- +static void _far _interrupt isrIrq05(void) { + _asm mov ax, seg ports + _asm mov ds, ax + _asm cld + serviceIrq(5); +} + + +// ----------------------------------------------------------------------- +// isrIrq07 - Hardware interrupt entry point for IRQ 7 (see isrIrq03) +// ----------------------------------------------------------------------- +static void _far _interrupt isrIrq07(void) { + _asm mov ax, seg ports + _asm mov ds, ax + _asm cld + serviceIrq(7); +} + + +// ----------------------------------------------------------------------- +// isrIrq09 - Hardware interrupt entry point for IRQ 9 (see isrIrq03) +// ----------------------------------------------------------------------- +static void _far _interrupt isrIrq09(void) { + _asm mov ax, seg ports + _asm mov ds, ax + _asm cld + serviceIrq(9); +} + + +// ----------------------------------------------------------------------- +// isrIrq10 - Hardware interrupt entry point for IRQ 10 (see isrIrq03) +// ----------------------------------------------------------------------- +static void _far _interrupt isrIrq10(void) { + _asm mov ax, seg ports + _asm mov ds, ax + _asm cld + serviceIrq(10); +} + + +// ----------------------------------------------------------------------- +// isrIrq11 - Hardware interrupt entry point for IRQ 11 (see isrIrq03) +// ----------------------------------------------------------------------- +static void _far _interrupt isrIrq11(void) { + _asm mov ax, seg ports + _asm mov ds, ax + _asm cld + serviceIrq(11); +} + + +// ----------------------------------------------------------------------- +// isrIrq12 - Hardware interrupt entry point for IRQ 12 (see isrIrq03) +// ----------------------------------------------------------------------- +static void _far _interrupt isrIrq12(void) { + _asm mov ax, seg ports + _asm mov ds, ax + _asm cld + serviceIrq(12); +} + + +// ----------------------------------------------------------------------- +// isrIrq15 - Hardware interrupt entry point for IRQ 15 (see isrIrq03) +// ----------------------------------------------------------------------- +static void _far _interrupt isrIrq15(void) { + _asm mov ax, seg ports + _asm mov ds, ax + _asm cld + serviceIrq(15); +} + + +// ----------------------------------------------------------------------- +// maskIrq - Mask or unmask an IRQ at its PIC; returns the previous state +// +// Called from application context only (re-enables interrupts). +// ----------------------------------------------------------------------- +static bool maskIrq(uint8_t irq, bool masked) { + uint16_t maskPort; + uint8_t bit; + uint8_t mask; + + maskPort = (irq >= IRQ_SLAVE_FIRST) ? PIC2_DATA : PIC1_DATA; + bit = (uint8_t)(1 << (irq % IRQ_SLAVE_FIRST)); + + _disable(); + mask = (uint8_t)_inp(maskPort); + if (masked) { + _outp(maskPort, mask | bit); + } else { + _outp(maskPort, mask & ~bit); + } + _enable(); + + return (mask & bit) != 0; +} + + +// ----------------------------------------------------------------------- +// pmVector - DPMI protected-mode interrupt number for an IRQ +// ----------------------------------------------------------------------- +static uint8_t pmVector(uint8_t irq) { + if (irq >= IRQ_SLAVE_FIRST) { + return (uint8_t)(PM_VECTOR_SLAVE + irq - IRQ_SLAVE_FIRST); + } + return (uint8_t)(PM_VECTOR_MASTER + irq); +} + + +// ----------------------------------------------------------------------- +// raiseEvents - Record events the application enabled with cevt +// ----------------------------------------------------------------------- +static void raiseEvents(PortStateT *port, uint16_t events) { + port->comDeb.evtWord |= events & port->comDeb.evtMask; +} + + +// ----------------------------------------------------------------------- +// reactivateIrqs - Unmask every IRQ we have hooked +// +// A DOS application run between Suspend/ReactivateOpenCommPorts may have +// masked them. +// ----------------------------------------------------------------------- +void reactivateIrqs(void) { + uint8_t irq; + + for (irq = 0; irq < IRQ_COUNT; irq++) { + if (irqHooks[irq].refCount > 0) { + maskIrq(irq, false); + } + } +} + + +// ----------------------------------------------------------------------- +// refreshNotify - Re-arm notifications after an application call, then +// post anything that is already true +// +// acked holds the CN_* bits the call acknowledged: CN_RECEIVE when the +// application read or discarded input, CN_EVENT when it fetched events. +// Re-arming on acknowledgment rather than on a level means data that +// arrived while the application was reading, or events it did not fetch, +// are announced again instead of stalling. CN_TRANSMIT re-arms once the +// output queue is refilled to its trigger. Interrupts disabled. +// ----------------------------------------------------------------------- +void refreshNotify(PortStateT *port, uint8_t acked) { + uint8_t sent; + + sent = (uint8_t)(port->notifySent & ~acked); + if (port->txNotifyTrigger != NOTIFY_OFF && port->comDeb.txCount >= (uint16_t)port->txNotifyTrigger) { + sent = (uint8_t)(sent & ~CN_TRANSMIT); + } + port->notifySent = sent; + + checkNotify(port); +} + + +// ----------------------------------------------------------------------- +// releaseRxFlow - Undo whatever was done to pause the remote sender +// +// Uses rxThrottle (what was actually done) and the current DCB's XON +// character, so applyDcb can call it before installing new settings. +// Interrupts disabled. +// ----------------------------------------------------------------------- +void releaseRxFlow(PortStateT *port) { + uint8_t throttle; + + throttle = port->rxThrottle; + if (throttle == 0) { + return; + } + + port->rxThrottle = 0; + if (throttle & RX_THROTTLE_XOFF) { + port->flowChar = (uint8_t)port->dcb.XonChar; + startTx(port); + } + if (throttle & (RX_THROTTLE_RTS | RX_THROTTLE_DTR)) { + writeMcr(port); + } +} + + +// ----------------------------------------------------------------------- +// serviceIrq - Service every open port on an IRQ, then EOI or chain +// +// With several UARTs on one edge-triggered line, passes repeat until a +// full pass finds nothing pending; otherwise an edge raised mid-service +// would be lost. An interrupt none of our ports claims is passed to the +// previous handler when another device may own the line (the IRQ was +// unmasked before we hooked it, or the ports are suspended for a DOS +// application). Otherwise it is a late edge from one of our own UARTs +// and only needs an EOI -- unless it is a spurious IRQ 7/15 that the PIC +// never put in service, which must not be acknowledged (a stray EOI +// would end some other interrupt early). +// ----------------------------------------------------------------------- +static void serviceIrq(uint8_t irq) { + IrqHookT *hook; + PortStateT *port; + uint16_t pass; + int16_t i; + bool handled; + bool found; + + hook = &irqHooks[irq]; + handled = false; + + if (!portsSuspended) { + for (pass = 0; pass < MAX_IRQ_PASSES; pass++) { + found = false; + for (i = 0; i < MAX_PORTS; i++) { + port = &ports[i]; + if (port->isOpen && port->irq == irq && servicePort(port)) { + found = true; + } + } + if (!found) { + break; + } + handled = true; + if (hook->refCount < 2) { + break; + } + } + } + + if (handled) { + eoiIrq(irq); + return; + } + + if (hook->prevIsr && (portsSuspended || !hook->wasMasked)) { + chainIsr(hook->prevIsr); + return; + } + + if ((irq == IRQ_SPURIOUS_PIC1 || irq == IRQ_SPURIOUS_PIC2) && !irqInService(irq)) { + if (irq == IRQ_SPURIOUS_PIC2) { + // The master did put the cascade input in service + _outp(PIC1_CMD, PIC_EOI); + } + return; + } + + eoiIrq(irq); +} + + +// ----------------------------------------------------------------------- +// servicePort - Handle every pending interrupt source on one UART +// +// Returns true if the UART had anything pending. +// ----------------------------------------------------------------------- +static bool servicePort(PortStateT *port) { + uint16_t base; + uint16_t loops; uint8_t iir; uint8_t lsr; - uint16_t base; + bool handled; - base = port->baseAddr; + base = port->comDeb.baseAddr; + handled = false; - for (;;) { + for (loops = 0; loops < MAX_IIR_LOOPS; loops++) { iir = (uint8_t)_inp(base + UART_IIR); - - // Bit 0 set = no interrupt pending - if (iir & IIR_PENDING) { + if (iir & IIR_NO_INT) { break; } + handled = true; switch (iir & IIR_ID_MASK) { case IIR_LSR: - // Line status: read LSR, accumulate errors lsr = (uint8_t)_inp(base + UART_LSR); - handleLsr(port, lsr); - // If data ready, also handle RX if (lsr & LSR_DR) { handleRx(port, lsr); + } else { + handleLsr(port, lsr); } break; case IIR_RDA: case IIR_TIMEOUT: - // Received data available or FIFO timeout - lsr = (uint8_t)_inp(base + UART_LSR); - handleRx(port, lsr); + handleRx(port, (uint8_t)_inp(base + UART_LSR)); break; case IIR_THRE: - // Transmitter holding register empty handleTx(port); break; case IIR_MSR: - // Modem status change handleMsr(port); break; + + default: + // Undefined source on a non-standard clone: clear status + (void)_inp(base + UART_LSR); + (void)_inp(base + UART_MSR); + break; } } - // Sync queue counts into ComDEB for third-party code - port->comDeb.qInCount = port->rxCount; - port->comDeb.qInHead = port->rxHead; - port->comDeb.qInTail = port->rxTail; - port->comDeb.qOutCount = port->txCount; - port->comDeb.qOutHead = port->txHead; - port->comDeb.qOutTail = port->txTail; + if (handled) { + checkNotify(port); + } + return handled; } // ----------------------------------------------------------------------- -// unhookIsr - Restore previous interrupt vector via DPMI +// setPmVector - Install a protected-mode interrupt vector (DPMI 0205h) +// ----------------------------------------------------------------------- +static bool setPmVector(uint8_t intNum, IsrProcT isr) { + uint16_t sel; + uint16_t off; + int16_t failed; + + sel = _FP_SEG(isr); + off = _FP_OFF(isr); + + _asm { + mov ax, DPMI_SET_PM_VECTOR + mov bl, intNum + mov cx, sel + mov dx, off + int DPMI_INT + sbb ax, ax + mov failed, ax + } + + return !failed; +} + + +// ----------------------------------------------------------------------- +// startTx - Make sure a THRE interrupt will come if anything can be sent +// +// Never writes THR: if THRE interrupts are already enabled, the one in +// progress will pick up the new data; if not, enabling them (0 -> 1 with +// the transmitter idle) raises one immediately. Interrupts disabled. +// ----------------------------------------------------------------------- +void startTx(PortStateT *port) { + if (!(port->comDeb.ierShadow & IER_THRE) && txSendable(port)) { + writeIer(port, (uint8_t)(port->comDeb.ierShadow | IER_THRE)); + } +} + + +// ----------------------------------------------------------------------- +// txSendable - True if handleTx would transmit something right now +// ----------------------------------------------------------------------- +static bool txSendable(PortStateT *port) { + if (port->txHold & TX_HOLD_HW) { + return false; + } + if (port->flowChar != NO_CHAR) { + return true; + } + return port->txHold == 0 && (port->txImmediate != NO_CHAR || port->comDeb.txCount > 0); +} + + +// ----------------------------------------------------------------------- +// unhookIsr - Release a port's IRQ +// +// The last port on an IRQ restores the PIC mask bit to its original state +// and puts the previous vector back; other ports on the IRQ keep running. // ----------------------------------------------------------------------- void unhookIsr(PortStateT *port) { - uint8_t intNum; - uint8_t picMask; - int16_t *refCount; - void (_far _interrupt **prevPtr)(void); + IrqHookT *hook; - intNum = port->irq + 8; - - if (port->irq == 3) { - prevPtr = &prevIsr3; - refCount = &irq3RefCount; - } else { - prevPtr = &prevIsr4; - refCount = &irq4RefCount; + hook = &irqHooks[port->irq]; + if (hook->refCount == 0) { + return; } - // Mask IRQ at PIC - picMask = _inp(PIC_DATA); - picMask |= port->irqMask; - _outp(PIC_DATA, picMask); - - (*refCount)--; - - // Only restore vector when last user of this IRQ unhooks - if (*refCount <= 0) { - uint16_t oldSeg = _FP_SEG(*prevPtr); - uint16_t oldOff = _FP_OFF(*prevPtr); - - _asm { - mov ax, 0205h - mov bl, intNum - mov cx, oldSeg - mov dx, oldOff - int 31h + hook->refCount--; + if (hook->refCount == 0) { + if (hook->wasMasked) { + maskIrq(port->irq, true); } - - *prevPtr = NULL; - *refCount = 0; + setPmVector(pmVector(port->irq), hook->prevIsr); + hook->prevIsr = NULL; } } // ----------------------------------------------------------------------- -// isr3 - ISR entry point for IRQ3 (COM2 and COM4) +// updateRxFlow - Pause or resume the remote sender from the RX fill level +// +// DCB limits: pause when no more than XoffLim bytes are free, resume once +// no more than XonLim bytes remain. Limits that do not fit the buffer +// are clamped. Each mechanism the DCB enables is applied (fInX sends +// XOFF, fRtsflow drops RTS, fDtrflow drops DTR) and recorded in +// rxThrottle so releaseRxFlow can undo exactly that. Interrupts disabled. // ----------------------------------------------------------------------- -void _far _interrupt isr3(void) { - int16_t i; +void updateRxFlow(PortStateT *port) { + uint16_t size; + uint16_t count; + uint16_t highWater; + uint16_t lowWater; + uint8_t throttle; - // Load DLL's data segment -- _interrupt saves/restores DS but doesn't - // set it. Without this, DS belongs to whatever code was interrupted - // and all global accesses (ports[], ring buffers) read garbage. - _asm mov ax, seg ports - _asm mov ds, ax + size = port->comDeb.rxSize; + if (size == 0) { + return; + } + count = port->comDeb.rxCount; - for (i = 0; i < MAX_PORTS; i++) { - if (ports[i].isOpen && ports[i].irq == 3) { - isrDispatch(&ports[i]); - } + highWater = (port->dcb.XoffLim < size) ? (uint16_t)(size - port->dcb.XoffLim) : size; + lowWater = port->dcb.XonLim; + if (lowWater >= highWater) { + lowWater = highWater / 2; } - // Send EOI to PIC - _outp(PIC_CMD, PIC_EOI); + if (port->rxThrottle == 0) { + if (count < highWater) { + return; + } + throttle = 0; + if (port->dcb.fInX) { + port->flowChar = (uint8_t)port->dcb.XoffChar; + throttle |= RX_THROTTLE_XOFF; + } + if (port->dcb.fRtsflow) { + throttle |= RX_THROTTLE_RTS; + } + if (port->dcb.fDtrflow) { + throttle |= RX_THROTTLE_DTR; + } + port->rxThrottle = throttle; + if (throttle & (RX_THROTTLE_RTS | RX_THROTTLE_DTR)) { + writeMcr(port); + } + if (throttle & RX_THROTTLE_XOFF) { + startTx(port); + } + } else if (count <= lowWater) { + releaseRxFlow(port); + } } // ----------------------------------------------------------------------- -// isr4 - ISR entry point for IRQ4 (COM1 and COM3) +// updateTxHold - Recompute the modem-line transmit holds from the MSR shadow +// +// A line holds transmission while low if the DCB uses it for output flow +// control: fOutxCtsFlow/fOutxDsrFlow, or an INFINITE Cts/Dsr/RlsTimeout +// (DDAG: an infinite timeout is how a DCB asks for CTS, DSR, or RLSD flow +// control). Finite timeouts are waited out at write time instead (see +// waitTxLines in commdrv.c). The XOFF hold is left alone (only XON, +// SETXON, or applyDcb clear it). Restarts the transmitter if a hold was +// released. Interrupts disabled. // ----------------------------------------------------------------------- -void _far _interrupt isr4(void) { - int16_t i; +void updateTxHold(PortStateT *port) { + uint8_t msr; + uint8_t hold; - // Load DLL's data segment -- see comment in isr3 - _asm mov ax, seg ports - _asm mov ds, ax + msr = port->comDeb.msrShadow; + hold = (uint8_t)(port->txHold & TX_HOLD_XOFF); - isrHitCount++; - - for (i = 0; i < MAX_PORTS; i++) { - if (ports[i].isOpen && ports[i].irq == 4) { - isrDispatch(&ports[i]); - } + if ((port->dcb.fOutxCtsFlow || port->dcb.CtsTimeout == INFINITE) && !(msr & MSR_CTS)) { + hold |= TX_HOLD_CTS; + } + if ((port->dcb.fOutxDsrFlow || port->dcb.DsrTimeout == INFINITE) && !(msr & MSR_DSR)) { + hold |= TX_HOLD_DSR; + } + if (port->dcb.RlsTimeout == INFINITE && !(msr & MSR_DCD)) { + hold |= TX_HOLD_RLSD; } - // Send EOI to PIC - _outp(PIC_CMD, PIC_EOI); + port->txHold = hold; + startTx(port); +} + + +// ----------------------------------------------------------------------- +// writeIer - Program IER and its shadow (interrupts disabled) +// ----------------------------------------------------------------------- +void writeIer(PortStateT *port, uint8_t ier) { + port->comDeb.ierShadow = ier; + _outp(port->comDeb.baseAddr + UART_IER, ier); +} + + +// ----------------------------------------------------------------------- +// writeMcr - Program MCR from the wanted DTR/RTS state and RX throttling +// +// OUT2 stays on while the port is open (it gates the UART interrupt). +// Interrupts disabled. +// ----------------------------------------------------------------------- +void writeMcr(PortStateT *port) { + uint8_t mcr; + + mcr = MCR_OUT2; + if (port->dtrOn && !(port->rxThrottle & RX_THROTTLE_DTR)) { + mcr |= MCR_DTR; + } + if (port->rtsOn && !(port->rxThrottle & RX_THROTTLE_RTS)) { + mcr |= MCR_RTS; + } + + port->comDeb.mcrShadow = mcr; + _outp(port->comDeb.baseAddr + UART_MCR, mcr); } diff --git a/drv/lpt.c b/drv/lpt.c new file mode 100644 index 0000000..ac68d86 --- /dev/null +++ b/drv/lpt.c @@ -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; +} diff --git a/drv/makefile b/drv/makefile index 5c408b5..0de96df 100644 --- a/drv/makefile +++ b/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 # ----------------------------------------------------------------------- diff --git a/drv/ring0.md b/drv/ring0.md new file mode 100644 index 0000000..56cfddb --- /dev/null +++ b/drv/ring0.md @@ -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