Compare commits

...

4 commits

Author SHA1 Message Date
c8ccedf569 Change default FIFO trigger from 8 to 1, yield CPU when idle
Default RX FIFO trigger level of 8 (FCR_TRIG_8) caused exactly 8
characters to buffer in hardware before the ISR fired. Change default
to 1 (FCR_TRIG_1) for responsive single-character interrupts while
keeping the 16-byte FIFO enabled as a safety buffer. COMnRxTRIGGER
SYSTEM.INI key still allows override.

The PM_NOYIELD polling loop never yielded the CPU timeslice, starving
other Windows applications. Add Yield call when no serial data is
flowing so other apps get CPU time. During bulk data flow HasData
stays true and the loop runs at full speed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 20:44:14 -06:00
fbf4ed7c40 Fix input lag: add PM_NOYIELD, skip idle GetDC, eliminate redundant renders
PeekMessage without PM_NOYIELD surrenders the timeslice on every empty
queue check (~55ms per yield in Win16).  Adding pm_NoYield keeps the
polling loop hot so keystrokes and serial echoes are processed without
scheduler delays.

FlipToScreen was calling GetDC/ReleaseDC on every loop iteration even
with zero dirty rows.  Added early-exit scan before acquiring a DC.

TickBlink was calling FlipToScreen redundantly (main loop also calls it).
Removed the FlipToScreen from TickBlink and reordered the main loop to
TickBlink (dirty only) then FlipToScreen (single render pass).

Also: removed FBlinkOn := True reset from ParseData (was dirtying the
cursor row on every incoming chunk), added WriteDeferred for parse-only
without render, moved FlipToScreen from private to public, added Show
call before entering the polling loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:28:56 -06:00
ec0ec8f074 Replace event-driven WM_COMMNOTIFY architecture with polling main loop
The ISR still fills the ring buffer (mandatory for 115200 baud), but the
app now polls ReadComm directly via a PeekMessage loop instead of waiting
for WM_COMMNOTIFY.  Blink uses GetTickCount instead of WM_TIMER.  This
eliminates all Windows message overhead from the data path while keeping
the message loop alive for keyboard, paint, and scrollbar.

Removed from KPCOMM.PAS: NotifyWndProc, hidden notification window,
RegisterClass/CreateWindow, EnableCommNotification, SetCommEventMask,
DoCommEvent, Process*Notify methods, OnComm/CommEvent/RThreshold/
SThreshold properties, modem shadow state (CTS/DSR/CD).

Removed from KPANSI.PAS: WM_TIMER handler, SetTimer/KillTimer, replaced
with public TickBlink method using GetTickCount at 500ms intervals.

Removed from drv/isr.c: checkNotify function and its call from
isrDispatch.  Removed from drv/commdrv.c: pfnPostMessage, all
rxNotifySent/txNotifySent edge-trigger bookkeeping, gutted
enableNotification to a no-op API-compat stub.  Removed from
drv/commdrv.h: rxNotifySent/txNotifySent fields (shifts struct layout),
PostMessageProcT typedef, pfnPostMessage extern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:01:40 -06:00
acf1a6b691 Remove BeginUpdate/EndUpdate, fix rendering starvation, add variable docs
Remove BeginUpdate/EndUpdate batching from TKPAnsi entirely -- Write now
renders immediately via FlipToScreen after every ParseData call.  Remove
FPendingScroll (caused rendering deadlock: EndUpdate refused to call
FlipToScreen while FPendingScroll > 0, but only FlipToScreen cleared it).
DoScrollUp simplified to set FAllDirty directly.

CommEvent drain loop retained (required by edge-triggered CN_RECEIVE) but
each chunk renders immediately -- no deferred batching.  Edge-triggered
notifications verified starvation-free at all levels: ISR, driver, KPCOMM
dispatch, terminal rendering, and keyboard output path.

Add comprehensive variable comments to all project files: TKPAnsi (44
fields), TKPComm (23 fields), TMainForm (9 fields), PortStateT, and
driver globals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 18:34:19 -06:00
7 changed files with 406 additions and 593 deletions

View file

@ -45,48 +45,77 @@ type
TKPAnsi = class(TCustomControl) TKPAnsi = class(TCustomControl)
private private
FScreen: TList; { Terminal buffer state }
FScrollback: TList; FScreen: TList; { Active screen lines (FRows PTermLine ptrs) }
FCursorRow: Integer; FScrollback: TList; { Scrollback history (up to FScrollbackSize) }
FCursorCol: Integer;
FSaveCurRow: Integer; { Cursor position (0-based row/col within the active screen) }
FSaveCurCol: Integer; FCursorRow: Integer; { Current row (0 = top) }
FAttrFG: Integer; FCursorCol: Integer; { Current column (0 = left) }
FAttrBG: Integer; FSaveCurRow: Integer; { Saved row for SCP/RCP (ESC[s / ESC[u) }
FAttrBold: Boolean; FSaveCurCol: Integer; { Saved column for SCP/RCP }
FAttrBlink: Boolean;
FAttrReverse: Boolean; { Current text attributes (set by SGR escape sequences) }
FParseState: TParseState; FAttrFG: Integer; { Foreground color index 0-7 }
FParamStr: string; FAttrBG: Integer; { Background color index 0-7 }
FMusicStr: string; FAttrBold: Boolean; { Bold: maps FG to bright (index + 8) }
FCellWidth: Integer; FAttrBlink: Boolean; { Blink: cell toggles visibility on timer }
FCellHeight: Integer; FAttrReverse: Boolean; { Reverse video: swap FG/BG at render time }
FBlinkOn: Boolean;
FTimerActive: Boolean; { ANSI escape sequence parser state }
FScrollPos: Integer; FParseState: TParseState; { Current parser state machine position }
FWrapMode: Boolean; FParamStr: string; { Accumulated CSI parameter digits/semicolons }
FCols: Integer; FMusicStr: string; { Accumulated ANSI music string (ESC[M..^N) }
FRows: Integer;
FScrollbackSize: Integer; { Font metrics (measured from OEM charset paint font) }
FCursorVisible: Boolean; FCellWidth: Integer; { Character cell width in pixels (typically 8) }
FLastCursorRow: Integer; FCellHeight: Integer; { Character cell height in pixels (typ 12-16) }
FOnKeyData: TKeyDataEvent;
FPaintFont: HFont; { Blink state }
FStockFont: Boolean; FBlinkOn: Boolean; { Cursor blink phase: True=visible }
FBlinkCount: Integer; FLastBlinkTick: Longint; { GetTickCount value at last blink toggle }
FUpdateCount: Integer;
FPendingScroll: Integer; { Scrollback view }
FDirtyRow: array[0..255] of Boolean; FScrollPos: Integer; { Lines scrolled back (0=live, >0=viewing history) }
FAllDirty: Boolean;
FTextBlinkOn: Boolean; { Terminal modes }
FGlyphBufH: THandle; FWrapMode: Boolean; { Auto-wrap at right margin (DEC ?7h/l) }
FGlyphBuf: Pointer;
FRowBufH: THandle; { Terminal dimensions }
FRowBuf: Pointer; FCols: Integer; { Number of columns (default 80) }
FDibInfo: TDibInfo; FRows: Integer; { Number of rows (default 25) }
FRowBufSize: Integer; FScrollbackSize: Integer; { Max scrollback lines to retain (default 500) }
FNibbleFG: Byte;
FNibbleBG: Byte; { Cursor visibility (DEC ?25h/l) }
FCursorVisible: Boolean; { True if cursor is shown }
FLastCursorRow: Integer; { Previous cursor row for ghost cleanup }
{ Events }
FOnKeyData: TKeyDataEvent; { Keyboard data callback (keys -> serial) }
{ Paint font }
FPaintFont: HFont; { GDI font handle for OEM_CHARSET rendering }
FStockFont: Boolean; { True if FPaintFont is a stock object (no delete) }
{ Dirty tracking: per-row flags for incremental rendering }
FDirtyRow: array[0..255] of Boolean; { True = row needs re-render }
FAllDirty: Boolean; { True = all rows need re-render }
FScrollbarDirty: Boolean; { True = scrollbar range/position needs update }
FTextBlinkOn: Boolean; { Text blink phase: True=visible, False=hidden }
{ Font atlas: glyph bitmaps + nibble lookup table (GlobalAlloc) }
FGlyphBufH: THandle; { GlobalAlloc handle for glyph block (8256 bytes) }
FGlyphBuf: Pointer; { Far ptr: offset 0..63 = nibble table, 64+ = glyphs }
{ Row pixel buffer: reusable 8bpp DIB for one terminal row }
FRowBufH: THandle; { GlobalAlloc handle for pixel buffer }
FRowBuf: Pointer; { Far ptr to pixel data (cols*cellW*cellH bytes) }
FDibInfo: TDibInfo; { BITMAPINFO with 16-color ANSI palette }
FRowBufSize: Integer; { Pixel buffer size in bytes }
{ Nibble table color cache: avoids rebuild when colors unchanged }
FNibbleFG: Byte; { FG index currently in nibble table (255=invalid) }
FNibbleBG: Byte; { BG index currently in nibble table (255=invalid) }
procedure AllocLine(Line: PTermLine); procedure AllocLine(Line: PTermLine);
procedure BuildAtlas; procedure BuildAtlas;
procedure ClearLine(Line: PTermLine); procedure ClearLine(Line: PTermLine);
@ -105,7 +134,6 @@ type
procedure EraseLine(Mode: Integer); procedure EraseLine(Mode: Integer);
procedure ExecuteCSI(FinalCh: Char); procedure ExecuteCSI(FinalCh: Char);
procedure ExecuteMusic; procedure ExecuteMusic;
procedure FlipToScreen;
procedure FreeLineList(List: TList); procedure FreeLineList(List: TList);
function GetCursorCol: Integer; function GetCursorCol: Integer;
function GetCursorRow: Integer; function GetCursorRow: Integer;
@ -126,7 +154,6 @@ type
procedure UpdateScrollbar; procedure UpdateScrollbar;
procedure WMEraseBkgnd(var Msg: TWMEraseBkgnd); message wm_EraseBkgnd; procedure WMEraseBkgnd(var Msg: TWMEraseBkgnd); message wm_EraseBkgnd;
procedure WMGetDlgCode(var Msg: TMessage); message wm_GetDlgCode; procedure WMGetDlgCode(var Msg: TMessage); message wm_GetDlgCode;
procedure WMTimer(var Msg: TWMTimer); message wm_Timer;
procedure WMVScroll(var Msg: TWMScroll); message wm_VScroll; procedure WMVScroll(var Msg: TWMScroll); message wm_VScroll;
protected protected
procedure CreateParams(var Params: TCreateParams); override; procedure CreateParams(var Params: TCreateParams); override;
@ -136,11 +163,12 @@ type
public public
constructor Create(AOwner: TComponent); override; constructor Create(AOwner: TComponent); override;
destructor Destroy; override; destructor Destroy; override;
procedure BeginUpdate;
procedure Clear; procedure Clear;
procedure EndUpdate;
procedure Reset; procedure Reset;
procedure FlipToScreen;
procedure TickBlink;
procedure Write(const S: string); procedure Write(const S: string);
procedure WriteDeferred(const S: string);
property CursorCol: Integer read GetCursorCol; property CursorCol: Integer read GetCursorCol;
property CursorRow: Integer read GetCursorRow; property CursorRow: Integer read GetCursorRow;
published published
@ -180,10 +208,8 @@ const
$00FFFFFF { 15 White (bright) } $00FFFFFF { 15 White (bright) }
); );
{ Blink timer interval. Win16 minimum is ~55 ms (18.2 Hz tick). } { Blink toggle interval in milliseconds (cursor + text blink). }
BlinkTimerMs = 55; BlinkMs = 500;
{ Cursor and text blink toggle every 9 timer ticks (~500 ms). }
BlinkInterval = 9;
{ OUT_RASTER_PRECIS may not be defined in Delphi 1.0 WinTypes } { OUT_RASTER_PRECIS may not be defined in Delphi 1.0 WinTypes }
OutRasterPrecis = 6; OutRasterPrecis = 6;
@ -263,12 +289,6 @@ begin
end; end;
procedure TKPAnsi.BeginUpdate;
begin
Inc(FUpdateCount);
end;
procedure TKPAnsi.BuildAtlas; procedure TKPAnsi.BuildAtlas;
{ Render all 256 CP437 characters into a monochrome bitmap, then extract } { Render all 256 CP437 characters into a monochrome bitmap, then extract }
{ per-glyph pixel masks into the glyph block at offset 64. Each glyph } { per-glyph pixel masks into the glyph block at offset 64. Each glyph }
@ -460,15 +480,13 @@ begin
FCellWidth := 8; FCellWidth := 8;
FCellHeight := 16; FCellHeight := 16;
FBlinkOn := True; FBlinkOn := True;
FTimerActive := False; FLastBlinkTick := GetTickCount;
FScrollPos := 0; FScrollPos := 0;
FWrapMode := True; FWrapMode := True;
FPaintFont := 0; FPaintFont := 0;
FStockFont := False; FStockFont := False;
FBlinkCount := 0;
FUpdateCount := 0;
FPendingScroll := 0;
FAllDirty := True; FAllDirty := True;
FScrollbarDirty := False;
FTextBlinkOn := True; FTextBlinkOn := True;
FRowBufSize := 0; FRowBufSize := 0;
FGlyphBufH := 0; FGlyphBufH := 0;
@ -625,11 +643,6 @@ end;
destructor TKPAnsi.Destroy; destructor TKPAnsi.Destroy;
begin begin
if FTimerActive and HandleAllocated then
begin
KillTimer(Handle, 1);
FTimerActive := False;
end;
DestroyRowBuffers; DestroyRowBuffers;
if (FPaintFont <> 0) and not FStockFont then if (FPaintFont <> 0) and not FStockFont then
begin begin
@ -730,14 +743,12 @@ begin
FScreen.Insert(0, Line); FScreen.Insert(0, Line);
{ Scroll down is rare; just repaint everything } { Scroll down is rare; just repaint everything }
FAllDirty := True; FAllDirty := True;
FPendingScroll := 0;
end; end;
procedure TKPAnsi.DoScrollUp; procedure TKPAnsi.DoScrollUp;
var var
Line: PTermLine; Line: PTermLine;
I: Integer;
begin begin
if FScreen.Count < FRows then if FScreen.Count < FRows then
Exit; Exit;
@ -745,39 +756,19 @@ begin
Line := FScreen[0]; Line := FScreen[0];
FScrollback.Add(Line); FScrollback.Add(Line);
FScreen.Delete(0); FScreen.Delete(0);
TrimScrollback; { TrimScrollback deferred to ParseData for batching }
{ Add blank line at bottom } { Add blank line at bottom }
GetMem(Line, SizeOf(TTermLineRec)); GetMem(Line, SizeOf(TTermLineRec));
AllocLine(Line); AllocLine(Line);
FScreen.Add(Line); FScreen.Add(Line);
UpdateScrollbar; FScrollbarDirty := True;
Inc(FPendingScroll); { Without ScrollDC, all rows must be re-rendered after a scroll }
if FPendingScroll >= FRows then { because the on-screen pixels haven't moved to match FScreen. }
begin
{ Scrolled more than one screen; just repaint everything }
FAllDirty := True; FAllDirty := True;
FPendingScroll := 0;
end
else
begin
{ Shift dirty flags up to match the scrolled line positions }
for I := 1 to FRows - 1 do
FDirtyRow[I - 1] := FDirtyRow[I];
FDirtyRow[FRows - 1] := True;
end;
end; end;
procedure TKPAnsi.EndUpdate;
begin
Dec(FUpdateCount);
if FUpdateCount <= 0 then
begin
FUpdateCount := 0;
FlipToScreen;
end;
end;
procedure TKPAnsi.EraseDisplay(Mode: Integer); procedure TKPAnsi.EraseDisplay(Mode: Integer);
@ -1232,8 +1223,7 @@ procedure TKPAnsi.FlipToScreen;
var var
DC: HDC; DC: HDC;
Row: Integer; Row: Integer;
R: TRect; HasDirty: Boolean;
ScrollY: Integer;
begin begin
if not HandleAllocated then if not HandleAllocated then
Exit; Exit;
@ -1242,26 +1232,16 @@ begin
if FRowBuf = nil then if FRowBuf = nil then
Exit; Exit;
{ Scrollback view: force full redraw, ignore pending scroll } { Scrollback view: force full redraw (line mapping changes) }
if FScrollPos <> 0 then if FScrollPos <> 0 then
begin
FAllDirty := True; FAllDirty := True;
FPendingScroll := 0;
end;
{ Deferred scroll: shift existing screen pixels up } { Deferred scrollbar update (batched from DoScrollUp) }
if (FPendingScroll > 0) and not FAllDirty then if FScrollbarDirty then
begin begin
R.Left := 0; UpdateScrollbar;
R.Top := 0; FScrollbarDirty := False;
R.Right := FCols * FCellWidth;
R.Bottom := FRows * FCellHeight;
ScrollY := FPendingScroll * FCellHeight;
DC := GetDC(Handle);
ScrollDC(DC, 0, -ScrollY, R, R, 0, nil);
ReleaseDC(Handle, DC);
end; end;
FPendingScroll := 0;
{ Dirty old cursor row to erase ghost when cursor moved between rows } { Dirty old cursor row to erase ghost when cursor moved between rows }
if FCursorRow <> FLastCursorRow then if FCursorRow <> FLastCursorRow then
@ -1273,6 +1253,22 @@ begin
FLastCursorRow := FCursorRow; FLastCursorRow := FCursorRow;
end; end;
{ Early exit: skip GetDC/ReleaseDC when nothing needs rendering }
if not FAllDirty then
begin
HasDirty := False;
for Row := 0 to FRows - 1 do
begin
if FDirtyRow[Row] then
begin
HasDirty := True;
Break;
end;
end;
if not HasDirty then
Exit;
end;
{ Interleaved render + blast: single buffer is reused per row } { Interleaved render + blast: single buffer is reused per row }
DC := GetDC(Handle); DC := GetDC(Handle);
for Row := 0 to FRows - 1 do for Row := 0 to FRows - 1 do
@ -1488,7 +1484,6 @@ begin
Exit; Exit;
{ Full repaint: render each row into the shared buffer and blast it } { Full repaint: render each row into the shared buffer and blast it }
FPendingScroll := 0;
FAllDirty := True; FAllDirty := True;
for Row := 0 to FRows - 1 do for Row := 0 to FRows - 1 do
@ -1509,28 +1504,90 @@ end;
procedure TKPAnsi.ParseData(const S: string); procedure TKPAnsi.ParseData(const S: string);
{ Process incoming data with an inlined fast path for printable characters. }
{ ~80% of BBS data is printable text in normal state. Inlining avoids the }
{ per-character method call to ProcessChar, and caching the Line pointer }
{ eliminates repeated TList lookups for consecutive chars on the same row. }
{ }
{ Does NOT call FlipToScreen -- the caller (Write) calls FlipToScreen }
{ after ParseData returns, ensuring immediate rendering. }
var var
I: Integer; I: Integer;
Ch: Char;
Line: PTermLine;
FGIdx: Byte;
BGIdx: Byte;
begin begin
Line := nil;
for I := 1 to Length(S) do for I := 1 to Length(S) do
ProcessChar(S[I]); begin
Ch := S[I];
{ Fast path: printable character in normal state }
if (FParseState = psNormal) and (Ch >= ' ') then
begin
if FCursorCol >= FCols then
begin
if FWrapMode then
begin
FCursorCol := 0;
Inc(FCursorRow);
if FCursorRow >= FRows then
begin
FCursorRow := FRows - 1;
DoScrollUp;
end;
Line := nil;
end
else
FCursorCol := FCols - 1;
end;
if Line = nil then
Line := FScreen[FCursorRow];
if FAttrBold then
FGIdx := FAttrFG + 8
else
FGIdx := FAttrFG;
BGIdx := FAttrBG;
if FAttrReverse then
begin
Line^.Cells[FCursorCol].FG := BGIdx;
Line^.Cells[FCursorCol].BG := FGIdx;
end
else
begin
Line^.Cells[FCursorCol].FG := FGIdx;
Line^.Cells[FCursorCol].BG := BGIdx;
end;
Line^.Cells[FCursorCol].Ch := Ch;
Line^.Cells[FCursorCol].Bold := FAttrBold;
Line^.Cells[FCursorCol].Blink := FAttrBlink;
FDirtyRow[FCursorRow] := True;
Inc(FCursorCol);
end
else
begin
{ Slow path: control chars, escape sequences }
Line := nil;
ProcessChar(Ch);
end;
end;
{ Deferred scrollback trim -- batched from DoScrollUp }
TrimScrollback;
{ Snap to bottom on new data } { Snap to bottom on new data }
if FScrollPos <> 0 then if FScrollPos <> 0 then
begin begin
FScrollPos := 0; FScrollPos := 0;
UpdateScrollbar; FScrollbarDirty := True;
FAllDirty := True; FAllDirty := True;
end; end;
{ Reset cursor blink to visible on new data }
FBlinkOn := True;
{ Render immediately -- no throttle. Data hits the screen as soon }
{ as it arrives. BeginUpdate/EndUpdate suppresses intermediate }
{ renders when the caller is batching multiple Write calls. }
if FUpdateCount = 0 then
FlipToScreen;
end; end;
@ -1818,13 +1875,6 @@ begin
CreateRowBuffers; CreateRowBuffers;
FAllDirty := True; FAllDirty := True;
{ Start render/blink timer }
if not FTimerActive then
begin
SetTimer(Handle, 1, BlinkTimerMs, nil);
FTimerActive := True;
end;
Invalidate; Invalidate;
end; end;
@ -2212,15 +2262,28 @@ end;
procedure TKPAnsi.TrimScrollback; procedure TKPAnsi.TrimScrollback;
{ Batch-optimized: free excess items, shift remainder down in one pass, }
{ then shrink from the end. O(n) total vs O(k*n) for k front-deletions. }
var var
Excess: Integer;
I: Integer;
Line: PTermLine; Line: PTermLine;
begin begin
while FScrollback.Count > FScrollbackSize do Excess := FScrollback.Count - FScrollbackSize;
if Excess <= 0 then
Exit;
{ Free the oldest lines }
for I := 0 to Excess - 1 do
begin begin
Line := FScrollback[0]; Line := FScrollback[I];
FreeMem(Line, SizeOf(TTermLineRec)); FreeMem(Line, SizeOf(TTermLineRec));
FScrollback.Delete(0);
end; end;
{ Shift remaining items down in one pass }
for I := 0 to FScrollback.Count - Excess - 1 do
FScrollback[I] := FScrollback[I + Excess];
{ Remove excess slots from the end (O(1) per deletion) }
for I := 1 to Excess do
FScrollback.Delete(FScrollback.Count - 1);
end; end;
@ -2257,20 +2320,18 @@ begin
end; end;
procedure TKPAnsi.WMTimer(var Msg: TWMTimer); procedure TKPAnsi.TickBlink;
var
Now: Longint;
begin begin
{ Blink counter: toggle cursor and text blink every BlinkInterval ticks } Now := GetTickCount;
Inc(FBlinkCount); if Now - FLastBlinkTick >= BlinkMs then
if FBlinkCount >= BlinkInterval then
begin begin
FBlinkCount := 0; FLastBlinkTick := Now;
FBlinkOn := not FBlinkOn; FBlinkOn := not FBlinkOn;
FTextBlinkOn := not FTextBlinkOn; FTextBlinkOn := not FTextBlinkOn;
DirtyBlinkRows; DirtyBlinkRows;
end; end;
{ Render blink changes and any stale dirty rows }
FlipToScreen;
end; end;
@ -2316,6 +2377,16 @@ end;
procedure TKPAnsi.Write(const S: string); procedure TKPAnsi.Write(const S: string);
begin
if Length(S) > 0 then
begin
ParseData(S);
FlipToScreen;
end;
end;
procedure TKPAnsi.WriteDeferred(const S: string);
begin begin
if Length(S) > 0 then if Length(S) > 0 then
ParseData(S); ParseData(S);

View file

@ -5,80 +5,54 @@ unit KPComm;
{ TKPComm is a non-visual TComponent descendant providing RS-232 serial } { TKPComm is a non-visual TComponent descendant providing RS-232 serial }
{ I/O via the Windows 3.1 comm API. Installs to the "KP" palette tab. } { I/O via the Windows 3.1 comm API. Installs to the "KP" palette tab. }
{ } { }
{ Port lifecycle: OpenComm -> BuildCommDCB + SetCommState -> } { Port lifecycle: OpenComm -> BuildCommDCB + SetCommState -> CloseComm. }
{ EnableCommNotification -> CloseComm. }
{ } { }
{ WM_COMMNOTIFY messages are received through a hidden utility window } { Data is read by polling Input (ReadComm) from a PeekMessage main loop }
{ with a registered class and dispatched to ProcessReceiveNotify, } { rather than through WM_COMMNOTIFY event dispatch. }
{ ProcessTransmitNotify, and ProcessEventNotify. }
{ }
{ Modem line status (CTS/DSR/CD) is tracked via shadow booleans toggled }
{ on transition events from GetCommEventMask, since the 16-bit comm API }
{ only provides transition events, not absolute line levels. }
interface interface
uses uses
SysUtils, Classes, WinTypes, WinProcs, Messages; SysUtils, Classes, WinTypes, WinProcs, Messages;
const
{ Communication events }
comEvReceive = 1;
comEvSend = 2;
comEvCTS = 3;
comEvDSR = 4;
comEvCD = 5;
comEvRing = 6;
comEvEOF = 7;
{ Error events }
comEvtBreak = 1001;
comEvtFrame = 1004;
comEvtOverrun = 1006;
comEvtRxOver = 1008;
comEvtParity = 1009;
comEvtTxFull = 1010;
type type
THandshaking = (hsNone, hsXonXoff, hsRtsCts, hsBoth); THandshaking = (hsNone, hsXonXoff, hsRtsCts, hsBoth);
TInputMode = (imText, imBinary); TInputMode = (imText, imBinary);
TKPComm = class(TComponent) TKPComm = class(TComponent)
private private
FCommId: Integer; { Port state }
FHWndNotify: HWnd; FCommId: Integer; { Comm port handle from OpenComm (-1 = closed) }
FCommPort: Integer;
FSettings: string; { Configuration (published properties) }
FPortOpen: Boolean; FCommPort: Integer; { Port number (1-based: 1=COM1, 2=COM2, ...) }
FInBufferSize: Integer; FSettings: string; { Baud/parity/data/stop string (e.g. '9600,N,8,1') }
FOutBufferSize: Integer; FPortOpen: Boolean; { True while port is open and operational }
FRThreshold: Integer; FInBufferSize: Integer; { Receive ring buffer size in bytes }
FSThreshold: Integer; FOutBufferSize: Integer; { Transmit ring buffer size in bytes }
FHandshaking: THandshaking; FHandshaking: THandshaking; { Flow control mode (none/XonXoff/RtsCts/both) }
FInputLen: Integer; FInputLen: Integer; { Max bytes per Input read (0=up to 255) }
FInputMode: TInputMode; FInputMode: TInputMode; { Text or binary read mode }
FDTREnable: Boolean;
FRTSEnable: Boolean; { Modem control lines }
FNullDiscard: Boolean; FDTREnable: Boolean; { DTR line state (True=asserted) }
FEOFEnable: Boolean; FRTSEnable: Boolean; { RTS line state (True=asserted) }
FParityReplace: string;
FBreakState: Boolean; { DCB options }
FCommEvent: Integer; FNullDiscard: Boolean; { Strip null bytes from received data }
FCTSState: Boolean; FEOFEnable: Boolean; { Detect EOF character in stream }
FDSRState: Boolean; FParityReplace: string; { Replacement char for parity errors ('' = none) }
FCDState: Boolean;
FOnComm: TNotifyEvent; { Runtime state }
FBreakState: Boolean; { True while break signal is being sent }
procedure ApplyHandshaking; procedure ApplyHandshaking;
procedure ApplyOptions; procedure ApplyOptions;
procedure ClosePort; procedure ClosePort;
procedure DoCommEvent(EventCode: Integer);
function GetInBufferCount: Integer; function GetInBufferCount: Integer;
function GetInput: string; function GetInput: string;
function GetOutBufferCount: Integer; function GetOutBufferCount: Integer;
procedure OpenPort; procedure OpenPort;
procedure ProcessEventNotify;
procedure ProcessReceiveNotify;
procedure ProcessTransmitNotify;
procedure SetBreak(Value: Boolean); procedure SetBreak(Value: Boolean);
procedure SetCommPort(Value: Integer); procedure SetCommPort(Value: Integer);
procedure SetDTREnable(Value: Boolean); procedure SetDTREnable(Value: Boolean);
@ -98,19 +72,13 @@ type
property Output: string write SetOutput; property Output: string write SetOutput;
property InBufferCount: Integer read GetInBufferCount; property InBufferCount: Integer read GetInBufferCount;
property OutBufferCount: Integer read GetOutBufferCount; property OutBufferCount: Integer read GetOutBufferCount;
property CDHolding: Boolean read FCDState;
property CTSHolding: Boolean read FCTSState;
property DSRHolding: Boolean read FDSRState;
property Break: Boolean read FBreakState write SetBreak; property Break: Boolean read FBreakState write SetBreak;
property CommEvent: Integer read FCommEvent;
published published
property CommPort: Integer read FCommPort write SetCommPort default 1; property CommPort: Integer read FCommPort write SetCommPort default 1;
property Settings: string read FSettings write SetSettings; property Settings: string read FSettings write SetSettings;
property PortOpen: Boolean read FPortOpen write SetPortOpen default False; property PortOpen: Boolean read FPortOpen write SetPortOpen default False;
property InBufferSize: Integer read FInBufferSize write SetInBufferSize default 4096; property InBufferSize: Integer read FInBufferSize write SetInBufferSize default 4096;
property OutBufferSize: Integer read FOutBufferSize write SetOutBufferSize default 4096; property OutBufferSize: Integer read FOutBufferSize write SetOutBufferSize default 4096;
property RThreshold: Integer read FRThreshold write FRThreshold default 0;
property SThreshold: Integer read FSThreshold write FSThreshold default 0;
property Handshaking: THandshaking read FHandshaking write SetHandshaking default hsNone; property Handshaking: THandshaking read FHandshaking write SetHandshaking default hsNone;
property InputLen: Integer read FInputLen write FInputLen default 0; property InputLen: Integer read FInputLen write FInputLen default 0;
property InputMode: TInputMode read FInputMode write FInputMode default imText; property InputMode: TInputMode read FInputMode write FInputMode default imText;
@ -119,7 +87,6 @@ type
property NullDiscard: Boolean read FNullDiscard write SetNullDiscard default False; property NullDiscard: Boolean read FNullDiscard write SetNullDiscard default False;
property EOFEnable: Boolean read FEOFEnable write FEOFEnable default False; property EOFEnable: Boolean read FEOFEnable write FEOFEnable default False;
property ParityReplace: string read FParityReplace write SetParityReplace; property ParityReplace: string read FParityReplace write SetParityReplace;
property OnComm: TNotifyEvent read FOnComm write FOnComm;
end; end;
procedure Register; procedure Register;
@ -127,11 +94,6 @@ procedure Register;
implementation implementation
const const
{ WM_COMMNOTIFY notification codes }
CN_RECEIVE = $0001;
CN_TRANSMIT = $0002;
CN_EVENT = $0004;
{ DCB Flags field bit masks. The Windows 3.1 DCB packs thirteen 1-bit } { 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. } { fields into a single UINT (Word) at offset 12 of the structure. }
{ Delphi 1.0 maps this UINT to TDCB.Flags. } { Delphi 1.0 maps this UINT to TDCB.Flags. }
@ -149,46 +111,6 @@ const
dcbDtrflow = $0800; dcbDtrflow = $0800;
dcbRtsflow = $1000; dcbRtsflow = $1000;
{ Hidden notification window class name }
NotifyClassName = 'KPCommNotify';
var
NotifyClassRegistered: Boolean;
{ ----------------------------------------------------------------------- }
{ Hidden notification window procedure }
{ }
{ Receives WM_COMMNOTIFY from the comm driver. Retrieves the TKPComm }
{ instance pointer stored in the window's extra bytes (offset 0) and }
{ dispatches to the appropriate Process* method. }
{ ----------------------------------------------------------------------- }
function NotifyWndProc(Wnd: HWnd; Msg: Word;
WParam: Word; LParam: Longint): Longint; export;
var
Comm: TKPComm;
NotifyCode: Word;
begin
if Msg = wm_CommNotify then
begin
Comm := TKPComm(GetWindowLong(Wnd, 0));
if (Comm <> nil) and Comm.FPortOpen and (Comm.FCommId >= 0) then
begin
NotifyCode := Word(LParam);
if (NotifyCode and CN_RECEIVE) <> 0 then
Comm.ProcessReceiveNotify;
if (NotifyCode and CN_TRANSMIT) <> 0 then
Comm.ProcessTransmitNotify;
if (NotifyCode and CN_EVENT) <> 0 then
Comm.ProcessEventNotify;
end;
Result := 0;
end
else
Result := DefWindowProc(Wnd, Msg, WParam, LParam);
end;
{ ----------------------------------------------------------------------- } { ----------------------------------------------------------------------- }
{ TKPComm } { TKPComm }
@ -265,7 +187,6 @@ end;
procedure TKPComm.ClosePort; procedure TKPComm.ClosePort;
begin begin
{ Set FPortOpen first to prevent stale WM_COMMNOTIFY processing }
FPortOpen := False; FPortOpen := False;
if FCommId >= 0 then if FCommId >= 0 then
@ -280,16 +201,6 @@ begin
CloseComm(FCommId); CloseComm(FCommId);
FCommId := -1; FCommId := -1;
end; end;
if FHWndNotify <> 0 then
begin
DestroyWindow(FHWndNotify);
FHWndNotify := 0;
end;
FCTSState := False;
FDSRState := False;
FCDState := False;
end; end;
@ -297,14 +208,11 @@ constructor TKPComm.Create(AOwner: TComponent);
begin begin
inherited Create(AOwner); inherited Create(AOwner);
FCommId := -1; FCommId := -1;
FHWndNotify := 0;
FCommPort := 1; FCommPort := 1;
FSettings := '9600,N,8,1'; FSettings := '9600,N,8,1';
FPortOpen := False; FPortOpen := False;
FInBufferSize := 4096; FInBufferSize := 4096;
FOutBufferSize := 4096; FOutBufferSize := 4096;
FRThreshold := 0;
FSThreshold := 0;
FHandshaking := hsNone; FHandshaking := hsNone;
FInputLen := 0; FInputLen := 0;
FInputMode := imText; FInputMode := imText;
@ -314,10 +222,6 @@ begin
FEOFEnable := False; FEOFEnable := False;
FParityReplace := '?'; FParityReplace := '?';
FBreakState := False; FBreakState := False;
FCommEvent := 0;
FCTSState := False;
FDSRState := False;
FCDState := False;
end; end;
@ -329,14 +233,6 @@ begin
end; end;
procedure TKPComm.DoCommEvent(EventCode: Integer);
begin
FCommEvent := EventCode;
if Assigned(FOnComm) then
FOnComm(Self);
end;
function TKPComm.GetInBufferCount: Integer; function TKPComm.GetInBufferCount: Integer;
var var
Stat: TComStat; Stat: TComStat;
@ -391,12 +287,9 @@ end;
procedure TKPComm.OpenPort; procedure TKPComm.OpenPort;
var var
WC: TWndClass;
DCB: TDCB; DCB: TDCB;
Buf: array[0..255] of Char; Buf: array[0..255] of Char;
Setting: string; Setting: string;
RxTh: Integer;
TxTh: Integer;
begin begin
{ Open the comm port } { Open the comm port }
StrPCopy(Buf, 'COM' + IntToStr(FCommPort)); StrPCopy(Buf, 'COM' + IntToStr(FCommPort));
@ -447,134 +340,10 @@ begin
else else
EscapeCommFunction(FCommId, CLRRTS); EscapeCommFunction(FCommId, CLRRTS);
{ Register notification window class (once per process) }
if not NotifyClassRegistered then
begin
FillChar(WC, SizeOf(WC), 0);
WC.lpfnWndProc := @NotifyWndProc;
WC.cbWndExtra := SizeOf(Longint);
WC.hInstance := HInstance;
WC.lpszClassName := NotifyClassName;
if not RegisterClass(WC) then
begin
CloseComm(FCommId);
FCommId := -1;
raise Exception.Create('RegisterClass failed');
end;
NotifyClassRegistered := True;
end;
{ Create hidden notification window and store Self for dispatch }
FHWndNotify := CreateWindow(NotifyClassName, '', ws_Popup,
0, 0, 0, 0, 0, 0, HInstance, nil);
if FHWndNotify = 0 then
begin
CloseComm(FCommId);
FCommId := -1;
raise Exception.Create('CreateWindow failed');
end;
SetWindowLong(FHWndNotify, 0, Longint(Self));
{ Enable event mask for modem status, errors, and breaks }
SetCommEventMask(FCommId,
ev_CTS or ev_DSR or ev_RLSD or ev_Ring or
ev_Err or ev_Break or ev_RxChar);
{ Enable comm notifications -- -1 disables the notification }
if FRThreshold > 0 then
RxTh := FRThreshold
else
RxTh := -1;
if FSThreshold > 0 then
TxTh := FSThreshold
else
TxTh := -1;
EnableCommNotification(FCommId, FHWndNotify, RxTh, TxTh);
{ Reset modem line shadow state }
FCTSState := False;
FDSRState := False;
FCDState := False;
FPortOpen := True; FPortOpen := True;
end; end;
procedure TKPComm.ProcessEventNotify;
var
EvtMask: Word;
Stat: TComStat;
ErrFlags: Integer;
begin
EvtMask := GetCommEventMask(FCommId,
ev_CTS or ev_DSR or ev_RLSD or ev_Ring or ev_Err or ev_Break);
if (EvtMask and ev_Break) <> 0 then
DoCommEvent(comEvtBreak);
{ Modem line shadow state: toggle on each transition event. }
{ The 16-bit comm API only provides transition events, not }
{ absolute line levels. }
if (EvtMask and ev_CTS) <> 0 then
begin
FCTSState := not FCTSState;
DoCommEvent(comEvCTS);
end;
if (EvtMask and ev_DSR) <> 0 then
begin
FDSRState := not FDSRState;
DoCommEvent(comEvDSR);
end;
if (EvtMask and ev_RLSD) <> 0 then
begin
FCDState := not FCDState;
DoCommEvent(comEvCD);
end;
if (EvtMask and ev_Ring) <> 0 then
DoCommEvent(comEvRing);
if (EvtMask and ev_Err) <> 0 then
begin
ErrFlags := GetCommError(FCommId, Stat);
if (ErrFlags and ce_Frame) <> 0 then
DoCommEvent(comEvtFrame);
if (ErrFlags and ce_Overrun) <> 0 then
DoCommEvent(comEvtOverrun);
if (ErrFlags and ce_RxOver) <> 0 then
DoCommEvent(comEvtRxOver);
if (ErrFlags and ce_RxParity) <> 0 then
DoCommEvent(comEvtParity);
if (ErrFlags and ce_TxFull) <> 0 then
DoCommEvent(comEvtTxFull);
end;
end;
procedure TKPComm.ProcessReceiveNotify;
begin
if FRThreshold <= 0 then
Exit;
{ WM_COMMNOTIFY with CN_RECEIVE means data is available -- the driver }
{ already checked the threshold. No need to call GetCommError here. }
DoCommEvent(comEvReceive);
end;
procedure TKPComm.ProcessTransmitNotify;
var
Stat: TComStat;
begin
if FSThreshold <= 0 then
Exit;
GetCommError(FCommId, Stat);
if Integer(Stat.cbOutQue) <= FSThreshold then
DoCommEvent(comEvSend);
end;
procedure TKPComm.SetBreak(Value: Boolean); procedure TKPComm.SetBreak(Value: Boolean);
begin begin
FBreakState := Value; FBreakState := Value;
@ -657,7 +426,7 @@ begin
begin begin
Written := WriteComm(FCommId, @Value[1], Length(Value)); Written := WriteComm(FCommId, @Value[1], Length(Value));
if Written < 0 then if Written < 0 then
DoCommEvent(comEvtTxFull); raise Exception.Create('WriteComm failed');
end; end;
end; end;

View file

@ -8,5 +8,5 @@ uses
begin begin
Application.CreateForm(TMainForm, MainForm); Application.CreateForm(TMainForm, MainForm);
Application.Run; MainForm.Run;
end. end.

View file

@ -5,8 +5,8 @@ unit TestMain;
{ } { }
{ Layout: toolbar row at top (port, settings, open/close, status), } { Layout: toolbar row at top (port, settings, open/close, status), }
{ TKPAnsi terminal filling the rest of the form. Received serial data } { TKPAnsi terminal filling the rest of the form. Received serial data }
{ is fed to the terminal via TKPAnsi.Write; keystrokes from the terminal } { is polled from TKPComm.Input in a PeekMessage main loop; keystrokes }
{ are sent out via TKPComm.Output. } { from the terminal are sent out via TKPComm.Output. }
interface interface
@ -17,22 +17,27 @@ uses
type type
TMainForm = class(TForm) TMainForm = class(TForm)
private private
FComm: TKPComm; { Components (owned by Self, freed automatically) }
FAnsi: TKPAnsi; FComm: TKPComm; { Serial communications component }
FLabelPort: TLabel; FAnsi: TKPAnsi; { ANSI terminal display }
FEditPort: TEdit;
FLabelSettings: TLabel; { Toolbar controls }
FEditSettings: TEdit; FLabelPort: TLabel; { "Port:" label }
FBtnOpen: TButton; FEditPort: TEdit; { COM port number entry }
FBtnClose: TButton; FLabelSettings: TLabel; { "Settings:" label }
FLabelStatus: TLabel; FEditSettings: TEdit; { Baud/parity/data/stop entry }
FBtnOpen: TButton; { Opens the serial port }
FBtnClose: TButton; { Closes the serial port }
FLabelStatus: TLabel; { Displays "Open" or "Closed" }
FDone: Boolean; { True when WM_QUIT received }
procedure AnsiKeyData(Sender: TObject; const Data: string); procedure AnsiKeyData(Sender: TObject; const Data: string);
procedure BtnCloseClick(Sender: TObject); procedure BtnCloseClick(Sender: TObject);
procedure BtnOpenClick(Sender: TObject); procedure BtnOpenClick(Sender: TObject);
procedure CommEvent(Sender: TObject);
procedure UpdateStatus; procedure UpdateStatus;
public public
constructor Create(AOwner: TComponent); override; constructor Create(AOwner: TComponent); override;
procedure Run;
end; end;
var var
@ -67,7 +72,6 @@ begin
try try
FComm.CommPort := StrToInt(FEditPort.Text); FComm.CommPort := StrToInt(FEditPort.Text);
FComm.Settings := FEditSettings.Text; FComm.Settings := FEditSettings.Text;
FComm.RThreshold := 1;
FComm.PortOpen := True; FComm.PortOpen := True;
except except
on E: Exception do on E: Exception do
@ -78,31 +82,6 @@ begin
end; end;
procedure TMainForm.CommEvent(Sender: TObject);
var
S: string;
begin
case FComm.CommEvent of
comEvReceive:
begin
{ Drain all available data in a single update batch. This }
{ suppresses per-Write rendering so we get one paint at the }
{ end instead of one per 255-byte chunk. }
FAnsi.BeginUpdate;
try
repeat
S := FComm.Input;
if Length(S) > 0 then
FAnsi.Write(S);
until Length(S) = 0;
finally
FAnsi.EndUpdate;
end;
end;
end;
end;
constructor TMainForm.Create(AOwner: TComponent); constructor TMainForm.Create(AOwner: TComponent);
begin begin
inherited CreateNew(AOwner); inherited CreateNew(AOwner);
@ -114,7 +93,6 @@ begin
{ Serial component } { Serial component }
FComm := TKPComm.Create(Self); FComm := TKPComm.Create(Self);
FComm.OnComm := CommEvent;
{ Row 1: Port and Settings } { Row 1: Port and Settings }
FLabelPort := TLabel.Create(Self); FLabelPort := TLabel.Create(Self);
@ -177,15 +155,66 @@ begin
{ Font diagnostic: write known CP437 box-drawing characters. } { Font diagnostic: write known CP437 box-drawing characters. }
{ If the OEM font is working, you should see: } { If the OEM font is working, you should see: }
{ Line 1: single-line box top ┌───┐ } { Line 1: single-line box top +---+ }
{ Line 2: shade + full block ░▒▓█ } { Line 2: shade + full block ░▒▓█ }
{ Line 3: single-line box bottom └───┘ } { Line 3: single-line box bottom +---+ }
{ If you see accented letters (Ú Ä ¿ ° ± ² Û À Ù), the font is } { If you see accented letters, the font is ANSI_CHARSET instead of }
{ ANSI_CHARSET instead of OEM_CHARSET. } { OEM_CHARSET. }
FAnsi.Write(#$DA#$C4#$C4#$C4#$BF' '#$B0#$B1#$B2#$DB' '#$C0#$C4#$C4#$C4#$D9#13#10); FAnsi.Write(#$DA#$C4#$C4#$C4#$BF' '#$B0#$B1#$B2#$DB' '#$C0#$C4#$C4#$C4#$D9#13#10);
end; end;
procedure TMainForm.Run;
var
Msg: TMsg;
S: string;
HasData: Boolean;
begin
Show;
FDone := False;
while not FDone do
begin
{ Process all pending Windows messages (keyboard, paint, scrollbar) }
while PeekMessage(Msg, 0, 0, 0, pm_Remove or pm_NoYield) do
begin
if Msg.message = wm_Quit then
begin
FDone := True;
Break;
end;
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
if FDone then
Break;
{ Poll serial data -- read one chunk per iteration }
HasData := False;
if FComm.PortOpen then
begin
S := FComm.Input;
if Length(S) > 0 then
begin
FAnsi.WriteDeferred(S);
HasData := True;
end;
end;
{ Tick blink (dirties rows if interval elapsed), then render }
FAnsi.TickBlink;
FAnsi.FlipToScreen;
{ Yield CPU to other apps when no serial data is flowing. }
{ PM_NOYIELD keeps message draining fast; Yield here gives other }
{ apps a timeslice only when idle. During bulk data flow, HasData }
{ stays True and the loop runs at full speed. }
if not HasData then
Yield;
end;
end;
procedure TMainForm.UpdateStatus; procedure TMainForm.UpdateStatus;
begin begin
if FComm.PortOpen then if FComm.PortOpen then

View file

@ -193,7 +193,7 @@ static void readPortConfig(int16_t commId, uint16_t FAR *baseAddr, uint8_t F
static uint16_t readSystemIni(const char FAR *section, const char FAR *key, uint16_t defVal); static uint16_t readSystemIni(const char FAR *section, const char FAR *key, uint16_t defVal);
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Port base addresses and IRQ table // Port base addresses and IRQ defaults (overridden by SYSTEM.INI)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
static const uint16_t portBase[MAX_PORTS] = { static const uint16_t portBase[MAX_PORTS] = {
COM1_BASE, COM2_BASE, COM3_BASE, COM4_BASE COM1_BASE, COM2_BASE, COM3_BASE, COM4_BASE
@ -204,25 +204,18 @@ static const uint8_t portIrq[MAX_PORTS] = {
}; };
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Global port state array // 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]; PortStateT ports[MAX_PORTS];
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Global instance handle // DLL instance handle (saved in LibMain for resource loading)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
static HANDLE ghInstance = NULL; static HANDLE ghInstance = NULL;
// ----------------------------------------------------------------------- // ISR hit counter for diagnostics (incremented in isr4, wraps at 65535)
// Dynamically resolved PostMessage
//
// comm.drv is loaded from [boot] before USER.EXE, so we can't have a
// static import for PostMessage. Resolve it on first use via
// GetProcAddress(GetModuleHandle("USER"), "PostMessage").
// -----------------------------------------------------------------------
PostMessageProcT pfnPostMessage = NULL;
// ISR hit counter for diagnostics
volatile uint16_t isrHitCount = 0; volatile uint16_t isrHitCount = 0;
@ -722,40 +715,27 @@ void enableFifo(PortStateT *port)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// enableNotification - Register window for WM_COMMNOTIFY (ordinal 100) // 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.
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
int16_t FAR PASCAL _export enableNotification(int16_t commId, HWND hwnd, int16_t rxThresh, int16_t txThresh) int16_t FAR PASCAL _export enableNotification(int16_t commId, HWND hwnd, int16_t rxThresh, int16_t txThresh)
{ {
PortStateT *port; PortStateT *port;
dbgInt16("KPCOMM: enableNotif Id", commId);
dbgHex16("KPCOMM: enableNotif hwnd", (uint16_t)hwnd);
dbgInt16("KPCOMM: enableNotif rxTh", rxThresh);
dbgInt16("KPCOMM: enableNotif txTh", txThresh);
if (commId < 0 || commId >= MAX_PORTS) { if (commId < 0 || commId >= MAX_PORTS) {
return FALSE; return FALSE;
} }
port = &ports[commId]; port = &ports[commId];
if (!port->isOpen) { if (!port->isOpen) {
dbgStr("KPCOMM: enableNotif not open\r\n");
return FALSE; return FALSE;
} }
// Lazily resolve PostMessage from USER.EXE
// (not available at boot when comm.drv is loaded from [boot])
if (!pfnPostMessage) {
HMODULE hUser = GetModuleHandle("USER");
if (hUser) {
pfnPostMessage = (PostMessageProcT)GetProcAddress(hUser, "PostMessage");
}
}
port->hwndNotify = hwnd; port->hwndNotify = hwnd;
port->rxNotifyThresh = rxThresh; port->rxNotifyThresh = rxThresh;
port->txNotifyThresh = txThresh; port->txNotifyThresh = txThresh;
dbgStr("KPCOMM: enableNotif OK\r\n");
return TRUE; return TRUE;
} }
@ -1063,21 +1043,21 @@ static void initPortState(PortStateT *port, int16_t commId)
trigKey[11] = 'E'; trigKey[11] = 'E';
trigKey[12] = 'R'; trigKey[12] = 'R';
trigKey[13] = '\0'; trigKey[13] = '\0';
rxTrigger = readSystemIni("386Enh", trigKey, 8); rxTrigger = readSystemIni("386Enh", trigKey, 1);
} }
switch (rxTrigger) { switch (rxTrigger) {
case 1:
port->fifoTrigger = FCR_TRIG_1;
break;
case 4: case 4:
port->fifoTrigger = FCR_TRIG_4; port->fifoTrigger = FCR_TRIG_4;
break; break;
case 8:
port->fifoTrigger = FCR_TRIG_8;
break;
case 14: case 14:
port->fifoTrigger = FCR_TRIG_14; port->fifoTrigger = FCR_TRIG_14;
break; break;
case 8: case 1:
default: default:
port->fifoTrigger = FCR_TRIG_8; port->fifoTrigger = FCR_TRIG_1;
break; break;
} }

View file

@ -322,86 +322,87 @@ typedef struct {
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Port state structure // 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().
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
typedef struct { typedef struct {
// Stock-compatible ComDEB -- must be kept synchronized. // Stock-compatible ComDEB -- must be at offset 0.
// cevt returns a FAR pointer to this. // cevt (SetCommEventMask) returns a FAR pointer to this.
// Third-party code reads msrShadow at offset 35 (KB Q101417).
ComDebT comDeb; ComDebT comDeb;
uint16_t baseAddr; // UART base I/O address // Hardware identification (from SYSTEM.INI or defaults)
uint8_t irq; // IRQ number (3 or 4) uint16_t baseAddr; // UART base I/O address (e.g. 0x03F8 for COM1)
int16_t commId; // Port ID (0=COM1, 1=COM2, ...) uint8_t irq; // IRQ number (3 for COM2/4, 4 for COM1/3)
uint8_t isOpen; // Port open flag int16_t commId; // Port index (0=COM1, 1=COM2, 2=COM3, 3=COM4)
uint8_t is16550; // 16550 FIFO detected uint8_t isOpen; // Nonzero while port is open (guards ISR dispatch)
uint8_t fifoEnabled; // FIFO enabled (COMnFIFO setting) uint8_t is16550; // Nonzero if 16550 FIFO detected at init
uint8_t fifoTrigger; // RX FIFO trigger FCR bits (FCR_TRIG_*) uint8_t fifoEnabled; // Nonzero if FIFO use enabled (COMnFIFO in SYSTEM.INI)
uint8_t fifoTrigger; // RX FIFO trigger level FCR bits (FCR_TRIG_*)
// Ring buffers (GlobalAlloc'd GMEM_FIXED) // Receive ring buffer (GlobalAlloc'd GMEM_FIXED for ISR stability)
HGLOBAL rxBufH; // Receive buffer handle HGLOBAL rxBufH; // GlobalAlloc handle (for GlobalFree on close)
uint8_t FAR *rxBuf; // Receive ring buffer uint8_t FAR *rxBuf; // Far pointer to buffer data
uint16_t rxSize; // Buffer size uint16_t rxSize; // Buffer capacity in bytes
uint16_t rxHead; // Write position (ISR writes) uint16_t rxHead; // Write position -- ISR increments
uint16_t rxTail; // Read position (app reads) uint16_t rxTail; // Read position -- reccom increments
uint16_t rxCount; // Bytes in buffer uint16_t rxCount; // Bytes in buffer (ISR increments, reccom decrements)
HGLOBAL txBufH; // Transmit buffer handle // Transmit ring buffer (GlobalAlloc'd GMEM_FIXED for ISR stability)
uint8_t FAR *txBuf; // Transmit ring buffer HGLOBAL txBufH; // GlobalAlloc handle (for GlobalFree on close)
uint16_t txSize; // Buffer size uint8_t FAR *txBuf; // Far pointer to buffer data
uint16_t txHead; // Write position (app writes) uint16_t txSize; // Buffer capacity in bytes
uint16_t txTail; // Read position (ISR reads) uint16_t txHead; // Write position -- sndcom increments
uint16_t txCount; // Bytes in buffer uint16_t txTail; // Read position -- ISR (handleTx) increments
uint16_t txCount; // Bytes in buffer (sndcom increments, ISR decrements)
// DCB shadow // Serial parameters (cached from DCB at inicom/setcom time)
uint16_t baudRate; // Current baud rate uint16_t baudRate; // Baud rate (raw or CBR_* index for >32767)
uint8_t byteSize; // Data bits (5-8) uint8_t byteSize; // Data bits per character (5-8)
uint8_t parity; // Parity mode uint8_t parity; // Parity mode (NOPARITY, ODDPARITY, EVENPARITY, ...)
uint8_t stopBits; // Stop bits uint8_t stopBits; // Stop bits (ONESTOPBIT, TWOSTOPBITS)
// Flow control state // Flow control state (managed by ISR and application code)
uint8_t hsMode; // Handshaking mode uint8_t hsMode; // Handshaking mode (HS_NONE/XONXOFF/RTSCTS/BOTH)
uint8_t txStopped; // TX halted by flow control uint8_t txStopped; // Nonzero = TX halted (received XOFF or CTS dropped)
uint8_t rxStopped; // We sent XOFF / dropped RTS uint8_t rxStopped; // Nonzero = we sent XOFF or dropped RTS
uint8_t xonChar; // XON character (default 0x11) uint8_t xonChar; // XON character to send/recognize (default 0x11 DC1)
uint8_t xoffChar; // XOFF character (default 0x13) uint8_t xoffChar; // XOFF character to send/recognize (default 0x13 DC3)
uint16_t xoffLim; // Send XOFF when rxCount > rxSize - xoffLim uint16_t xoffLim; // Assert flow control when rxCount > rxSize - xoffLim
uint16_t xonLim; // Send XON when rxCount < xonLim uint16_t xonLim; // Release flow control when rxCount < xonLim
// Modem control shadow // Modem control line shadow (tracks EscapeCommFunction calls)
uint8_t dtrState; // DTR line state uint8_t dtrState; // Nonzero = DTR is asserted
uint8_t rtsState; // RTS line state (when not flow-controlled) uint8_t rtsState; // Nonzero = RTS is asserted (when not flow-controlled)
// Error accumulator // Error accumulator (sticky CE_* bits, cleared by stacom/GetCommError)
uint16_t errorFlags; // CE_* error flags (sticky until read) uint16_t errorFlags; // Accumulated CE_RXOVER, CE_OVERRUN, CE_FRAME, etc.
// Event notification // WM_COMMNOTIFY event notification (stored for API compat, not used)
HWND hwndNotify; // Window for WM_COMMNOTIFY HWND hwndNotify; // Target window for PostMessage (0=disabled)
int16_t rxNotifyThresh; // CN_RECEIVE threshold (-1=disabled) int16_t rxNotifyThresh; // CN_RECEIVE threshold (-1=disabled)
int16_t txNotifyThresh; // CN_TRANSMIT threshold (-1=disabled) int16_t txNotifyThresh; // CN_TRANSMIT threshold (-1=disabled)
// ISR state // ISR chaining and PIC management
void (FAR *prevIsr)(void); // Previous ISR in chain void (FAR *prevIsr)(void); // Previous ISR vector (restored on unhook)
uint8_t irqMask; // PIC mask bit for this IRQ uint8_t irqMask; // PIC mask bit for this IRQ (1 << irq)
uint8_t breakState; // Break signal active uint8_t breakState; // Nonzero while break signal is active on line
// Priority transmit // Priority transmit (XON/XOFF flow control characters)
int16_t txImmediate; // -1=none, else char to send immediately int16_t txImmediate; // -1=none, 0..255=char to send before buffered data
// DCB copy for GetCommState // Full DCB copy (returned by getdcb, updated by setcom)
DCB dcb; // Full DCB for GETDCB/SETCOM DCB dcb;
} PortStateT; } PortStateT;
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Global port state array // Global port state array (one entry per COM port, indexed by commId)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
extern PortStateT ports[MAX_PORTS]; extern PortStateT ports[MAX_PORTS];
// ----------------------------------------------------------------------- // ISR hit counter for diagnostics (incremented in isr4, wraps at 65535)
// Dynamically resolved PostMessage (USER.EXE not loaded at boot)
// -----------------------------------------------------------------------
typedef BOOL (FAR PASCAL *PostMessageProcT)(HWND, UINT, WPARAM, LPARAM);
extern PostMessageProcT pfnPostMessage;
// ISR hit counter for diagnostics
extern volatile uint16_t isrHitCount; extern volatile uint16_t isrHitCount;
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------

View file

@ -10,7 +10,6 @@
// Prototypes // Prototypes
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
static void checkFlowRx(PortStateT *port); static void checkFlowRx(PortStateT *port);
static void checkNotify(PortStateT *port);
static void handleLsr(PortStateT *port, uint8_t lsr); static void handleLsr(PortStateT *port, uint8_t lsr);
static void handleMsr(PortStateT *port); static void handleMsr(PortStateT *port);
static void handleRx(PortStateT *port, uint8_t lsr); static void handleRx(PortStateT *port, uint8_t lsr);
@ -21,11 +20,17 @@ void _far _interrupt isr4(void);
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Saved previous ISR vectors for IRQ3 and IRQ4 // Saved previous ISR vectors for IRQ3 and IRQ4
//
// 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).
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
static void (_far _interrupt *prevIsr3)(void) = NULL; static void (_far _interrupt *prevIsr3)(void) = NULL; // Original IRQ3 vector
static void (_far _interrupt *prevIsr4)(void) = NULL; static void (_far _interrupt *prevIsr4)(void) = NULL; // Original IRQ4 vector
// Track how many ports are using each IRQ so we know when to unhook // 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 irq3RefCount = 0;
static int16_t irq4RefCount = 0; static int16_t irq4RefCount = 0;
@ -76,46 +81,6 @@ static void checkFlowRx(PortStateT *port)
} }
// -----------------------------------------------------------------------
// checkNotify - Post WM_COMMNOTIFY if threshold conditions met
//
// Called at end of ISR dispatch, outside the IIR loop.
// Uses PostMessage to avoid reentrancy issues.
// -----------------------------------------------------------------------
static void checkNotify(PortStateT *port)
{
uint16_t notifyBits;
if (!port->hwndNotify) {
return;
}
notifyBits = 0;
// CN_RECEIVE: rxCount crossed threshold from below
if (port->rxNotifyThresh >= 0 && port->rxCount >= (uint16_t)port->rxNotifyThresh) {
notifyBits |= CN_RECEIVE;
}
// CN_TRANSMIT: space available crossed threshold
if (port->txNotifyThresh >= 0) {
uint16_t txFree = port->txSize - port->txCount;
if (txFree >= (uint16_t)port->txNotifyThresh) {
notifyBits |= CN_TRANSMIT;
}
}
// CN_EVENT: any event bits accumulated
if (port->comDeb.evtWord & port->comDeb.evtMask) {
notifyBits |= CN_EVENT;
}
if (notifyBits && pfnPostMessage) {
pfnPostMessage(port->hwndNotify, WM_COMMNOTIFY, (WPARAM)port->commId, (LPARAM)notifyBits);
}
}
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// handleLsr - Process line status errors // handleLsr - Process line status errors
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@ -431,8 +396,6 @@ void isrDispatch(PortStateT *port)
port->comDeb.qOutCount = port->txCount; port->comDeb.qOutCount = port->txCount;
port->comDeb.qOutHead = port->txHead; port->comDeb.qOutHead = port->txHead;
port->comDeb.qOutTail = port->txTail; port->comDeb.qOutTail = port->txTail;
checkNotify(port);
} }