[Back to TSR SWAG index] [Back to Main SWAG index] [Original]
{
---------------------------------------------------------------------------
MD> How do you write something so it is interup driven?
MD> Can anyone post some simple code to perhaps count a number
MD> up by one and display it to the screen while at the same
MD> time allowing another part of the program do what ever it's
MD> suppose to.?
Marek:
The features to which you refer constitute writing a TSR program.
These programs are executed and terminate, but stay resident in
system memory. Prior to termination they insert themselves into
the Interrupt Service Routine chain of a known interrupt vector.
This programming was simplified somewhat in turbo Pascal Release
4 or 5 with the addition of the Keep, GetIntVec and SetIntVec
procedures of the DOS unit. However, these procedures are only
a small fraction of the coding needed to write reliable TSR's.
This fact explains the lack of simple code examples.
To write good TSR's you need to read about them and play with
someone else's code for a while. (I've coded pascal for 17 years,
but my first TSR took about 3 days to debug). For a solid
Pascal reference, try Tom Swan's 'Mastering Turbo Pascal'.
To figure out what the interrupt functions are doing, I recommend
Ralf Brown's & Jim Kyle's 'PC Interrupts'. The book is derived
from their well-known interrupt list (INTERnn.ZIP) available
on many programming oriented BBS systems. The index of the book
is well worth the money. Finally, I recommend Ed Mitchell's
"Borland Pascal Developer's Guide". Ed's TSR code is thorough
and documented with education in mind. Since I'm quoting
ED's book here, please buy a copy if you find the code useful.
Good luck
-------------------------------------------------------------------------
TSR Code Example
-------------------------------------------------------------------------
}
{ TSR.PAS
Sample TSR application written in Turbo Pascal.
IMPORTANT!
This TSR operates only in TEXT mode, and, as
written, supports only 80 x 25 sized screens
(not 43- or 50- line display modes).
This TSR will only operate on DOS 3.x or newer
versions of DOS.
Use this code as an example to write your own TSR code.
Modify the $M compiler directive, below, to specify
the maximum stack size, minimum heap and maximum heap
required for your TSR application.
Please see the text and other source comments
for important restrictions.
TSRs can be DANGEROUS, so be careful.
}
{$S-}
{$M 3072, 0, 512}
uses
Crt, Dos;
type
{ Defines an array to store the screen image.
For greater efficiency, your code may want to save only
the portion of the screen that is changed by your TSR
code. This implementation saves the entire screen image,
at popup time, for the greatest flexibility. }
TSavedVideo=Array[0..24, 0..79] of Word;
PSavedVideo=^TSavedVideo;
{ The following items define the TSR's identification
string. }
type
String8=String[8];
const
IdStr1:String8='TP7RTSR';
IdStr2:String8='TSRInUse';
var
CPURegisters: Registers;
{ General register structure for Intr calls. }
CursorStartLine: Byte;
{ Stores cursor shape information. }
CursorEndLine: Byte;
{ Stores cursor shape information. }
DiskInUse: Word;
{ Tracks INT 13 calls in progress. }
MadeActive: Boolean;
{ TRUE if this TSR has been asked to pop up. }
OurSP: Word;
OurSS: Word;
{ Saved copies of our SS and SP registers. }
PInt09: Pointer;
{ Saved address of keyboard handler. }
PInt12: Pointer;
{ Saved address of GetMemorySize interrupt. }
PInt1B: Pointer;
{ Ctrl-Break interrupt address. }
PInt24: Pointer;
{ DOS Critical error handler. }
PInt28: Pointer;
{ Saved address of background task scheduler. }
PInt1C: Pointer;
{ Saved address of timer handler. }
PInDosFlag: ^Word;
{ Points to DOS's InDos flag. }
VideoMem: PSavedVideo;
{ Points to actual video memory area. }
SavedVideo:TSavedVideo;
{ Stores the video memory when TSR is popped up. }
SavedWindMin: Word;
{ Holds saved copy of WindMin for restoring window. }
SavedWindMax: Word;
{ Holds saved copy of WindMax for restoring window. }
SavedSS,
SavedSP: Word;
{ Saves caller stack registers; must be global
to store in fixed memory location, not on local
stack of interrupted process. }
SavedX,
SavedY: Word;
{ Stores X, Y cursor prior to TSR popup }
{ for restoration when TSR goes away. }
TempPtr: Pointer;
{ Used internally to DoUnInstall. }
TSRInUse: Boolean;
{ Set TRUE during processing to avoid double
activation. }
procedure SaveDisplay;
{ Copies the content of video memory to an internal
array structure. Saves the cursor location and cursor
shape definitions. }
var
CursorLines: Byte;
begin
{ Save cursor location. }
SavedX := WhereX;
SavedY := WhereY;
{ Saved existing window values. }
SavedWindMin := WindMin;
SavedWindMax := WindMax;
{ Get and save current cursor shape. }
with CPURegisters do
begin
AH := $03;
BH := 0;
Intr($10, CPURegisters);
CursorStartLine := CH;
CursorEndLine:= CL;
end;
{ Get equipment-type information. If Monochrome
adapter in use, then point to $B000; otherwise use the
color memory area. }
Intr( $11, CPURegisters );
if ((CPURegisters.AX shr 4) and 7) = 3 then
begin
VideoMem := Ptr( $B000, 0 );
CursorLines := 15;
end
else
begin
VideoMem := Ptr( $B800, 0 );
CursorLines := 7;
end;
SavedVideo := VideoMem^;
{ Change cursor shape to block cursor. }
with CPURegisters do
begin
AH := $01;
CH := 0;
CL := CursorLines;
Intr($10, CPURegisters);
end;
end; { SaveDisplay }
procedure RestoreDisplay;
{ Always called sometime after calling SaveDisplay.
Restores the video display to its state prior to the
TSR popping up. }
begin
{ Restore screen content. }
VideoMem^ := SavedVideo;
{ Restore cursor shape. }
with CPURegisters do
begin
AH := $01;
CH := CursorStartLine;
CL := CursorEndLine;
Intr($10, CPURegisters);
end;
{ Resize window so the GotoXY (below) will work. }
Window (Lo(SavedWindMin)+1, Hi(SavedWindMin)+1,
Lo(SavedWindMax)+1, Hi(SavedWindMax)+1);
Gotoxy ( SavedX, SavedY );
end; { Restore Display }
procedure TrapCriticalErrors;
assembler;
{ INT 24H }
{
This handler is enabled only while the TSR is popped
up on-screen.
This handler exists solely to catch any DOS
critical errors and is a crude method of doing so. Since
this routine does nothing, any critical errors that
occur while the TSR is popped up are ignored--which
could be very dangerous. Also, if another TSR or ISR
pops up after this one, it may get the critical
error that was intended for this TSR.
NOTE: Normally, this could be an "interrupt" type
procedure. However, Turbo Pascal pushes and then pops
all registers prior to the IRET instruction. By writing
this as an assembler routine, this generates the IRET
directly, followed by one superfluous RET instruction
generated by the assembler, resulting in substantial
code savings.
}
asm
IRET
end; { TrapCriticalErrors }
procedure CBreakCheck;
assembler;
{ INT 1BH }
{ This routine results in a no operation; when hooked to
the INT 1B Ctrl-Break interrupt handler, it causes
nothing to happen when Ctrl-Break or Ctrl-C are pressed.
This routine is hooked only when the TSR is popped up. }
asm
IRET
end; { CBreakCheck }
function GetKey : Integer;
{ Pauses for input of a
single keystroke from the keyboard and returns the ASCII
value. In the case where an Extended keyboard key is
pressed, GetKey returns the ScanCode + 256. The Turbo
Pascal ReadKey function is called to perform the
keystroke input. This routine returns a 0 when an Extended
key has been typed (for example, left or right arrow)
and we must read the next byte to determine the Scan
code.
}
var
Ch : Char;
begin
{ While waiting for a key to be pressed, call
the INT $28 DOS Idle interrupt to allow background
tasks a chance to run. }
repeat
asm
int $28
end;
until KeyPressed;
Ch := ReadKey;
If Ord(Ch) <> 0 then
GetKey := Ord(Ch) { Return normal ASCII value. }
else
{ Read the DOS Extended SCAN code that follows. }
GetKey := Ord(ReadKey) + 256;
end;{GetKey}
procedure DoPopUpFunction;
{ This procedure is the "guts" of the popup
application. You can code your own application here, if
you want. Be sure to read the text for important
restrictions on what can be written in a TSR.
As implemented here, this popup displays a table
of ASCII values.
}
const
UpperLeftX=10;
UpperLeftY=5;
{ Define upper-left corner of TSR's popup window. }
LowerLeftX=70;
LowerRightY=20;
{ Define lower-right corner of TSR's popup window. }
Width=LowerLeftX - UpperLeftX + 1;
Height=LowerRightY - UpperLeftY + 1;
{ Calculated width and height of popup window. }
MinX=6;
{ Distance from left edge to display ASCII table. }
RightEdge=8;
{ Marks the right edge (Width-RightEdge) of table. }
MinY=4;
{ Distance from top of window to start ASCII Table. }
ValuesPerLine=Width-8 - MinX;
{ Computed number of ASCII values in each line. }
NumLines=255 div ValuesPerLine;
{ Computed number of lines in the ASCII table. }
KEY_LEFTARROW = 331;
{ Keystroke values for extended keyboard codes. }
KEY_RIGHTARROW = 333;
KEY_DOWNARROW = 336;
KEY_UPARROW = 328;
KEY_ESCAPE = 27;
KEY_ENTER = 13;
procedure PutChar( X, Y: Integer; ChCode: Char );
{ Writes the single character ChCode to the screen
at (X, Y), where (X, Y) is relative to the TSR popup
window. This routine is used for displaying the ASCII
table because the usual Pascal Write() translates ASCII
7 to a bell ring, and ASCII 13 and 10 to carriage return
and line feed. By using the PC BIOS routine directly, we
bypass Pascal's translation of these characters. }
begin
with CPURegisters do
begin
{ Move the cursor to adjusted (X, Y). }
AH := $02;
BH := 0;
DH := UpperLeftY + Y - 2;
DL := UpperLeftX + X - 2;
Intr($10, CPURegisters);
{ Output the character to the current cursor location. }
AH := $09;
AL := byte(ChCode);
BH := 0;
BL := 3 shl 4 + 14;
{ Background=color 3; Foreground=color 14 }
CX := 1;
Intr($10, CPURegisters);
end;
end; {PutChar}
var
ASCIICode: Integer;
{ Computed from X, Y location in table. }
I: Integer;
{ For loop index variable. }
TextLine: String[Width];
{ Buffer to hold width of window's text. }
X, Y: Integer;
{ Tracks cursor location in ASCII table. }
Ch : Integer;
{ Holds the keystroke typed. }
begin {DoPopUpFunction}
{ Select White text on Cyan background for
{ Set up a viewing window; makes calculation
of X, Y easier. }
Window(UpperLeftX, UpperLeftY, LowerLeftX, LowerRightY);
{ Enclose the window by drawing a border around
it and filling the interior with blanks. }
FillChar( TextLine[1], Width, ' ');
TextLine[0] := Chr( Width );
TextLine[1] := chr( 179 );
TextLine[Width] := chr( 179 );
for I := 2 to Height - 2 do
begin
Gotoxy(1, I);
Write( TextLine );
end;
FillChar( TextLine[1], Width, Chr(196));
TextLine[1] := Chr( 218 );
TextLine[Width] := Chr( 191 );
Gotoxy( 1 , 1 );
Write( TextLine );
TextLine[1] := Chr( 192 );
TextLine[Width] := Chr( 217 );
Gotoxy ( 1, Height - 1 );
Write( TextLine );
{ Display window title }
Gotoxy ( Width div 2 -10, 2 );
Write( 'Table of ASCII Values' );
{ Draw the ASCII table on the display }
X := MinX;
Y := MinY;
for I := 0 to 255 do
begin
PutChar( X, Y, Chr(I) );
Inc(X);
If X = (Width-RightEdge) then
begin
Inc( Y );
X := MinX;
end;
end;
Gotoxy ( Width div 2 - 20 , 11 );
Write('Use arrow keys to navigate; Esc when done');
X := MinX; Y := MinY;
repeat
{ Compute ASCII code and value at X, Y }
ASCIICode := (X-MinX + (Y-MinY)*ValuesPerLine);
Gotoxy (Width div 2 - 11 , 13);
{ NOTE: This allows display of
"ASCII codes" greater than 256 if cursor
moves into blank area in table. }
Write('Character= ', ' ASCII=',ASCIICode:3);
PutChar(Width div 2, 13, Chr(ASCIICode));
{ Display that value, below }
Gotoxy ( X, Y );
Ch := GetKey;
Case Ch Of
KEY_LEFTARROW: if X > MinX then Dec(X);
KEY_RIGHTARROW:
if X < (Width - RightEdge - 1) then Inc(X);
KEY_DOWNARROW: if Y < (MinY+NumLines) then Inc(Y);
KEY_UPARROW: if Y > MinY then Dec(Y);
end;
until Ch = 27;
{ End of TSR popup code. }
end; {DoPopUpFunction}
procedure RunPopUp;
{ Switches from system stack to TSR's stack. Calls
DoPopUpFunction to run the actual TSR application. This
keeps all the ugly details separate from the
application. Note that while the TSR is up on the
screen, and only while the TSR is up, we trap the
Ctrl-Break and DOS critical errors interrupt. We do
nothing when we see them except return, thereby ignoring
the interrupts. }
begin
{ Switch stacks. }
asm
CLI
end;
SavedSS := SSeg;
SavedSP := SPtr;
asm
MOV SS, OurSS
MOV SP, OurSP
STI
end;
GetIntVec( $1B, PInt1B );
{ Disable Ctrl-Break checking. }
SetIntVec( $1B, @CBreakCheck );
GetIntVec( $24, PInt24 );
{ Trap DOS critical errors. }
SetIntVec( $24, @TrapCriticalErrors );
SaveDisplay;
DoPopUpFunction;
RestoreDisplay;
SetIntVec( $24, PInt24 );
{ Reenable DOS critical error trapping. }
SetIntVec( $1B, PInt1B );
{ Reenable Ctrl-C trapping. }
{ Restore stacks. }
asm
CLI
MOV SS, SavedSS
MOV SP, SavedSP
STI
end;
end; {RunPopUp}
procedure BackgroundInt;
interrupt;
{ INT 28H }
{ This routine is hooked in the DOS INT 28H chain,
known variously as the DOSOK or DOSIdle interrupt. The
idea is that when applications are doing nothing except
waiting for a keystroke, they can call INT 28
repeatedly. INT 28 runs through a chain of applications
that each get a crack at running.
The keyboard interrupt handler watches for the
magic TSR popup key. Some of the time it runs the TSR
popup directly; when DOS is doing something, however, it
can't run the TSR. So, it sets a flag saying "Hey, INT
28, if you see this flag set, then do the TSR." So the
INT 28 code, here, examines the flag. If the flag is set, INT 28
knows that the TSR was activated, so it calls it now.
We can do this because DOS calls INT 28 only if it's safe
for something else to run.
}
begin
{ Call saved INT 28H handler. }
asm
PUSHF
CALL PInt28
end;
if MadeActive then
begin
TSRInUse := True;
MadeActive := False;
RunPopUp;
TSRInUse := False;
end;
end; {BackgroundInt}
procedure KeyboardInt;
interrupt;
{ INT 09H }
{ Examines all keyboard interrupts. First calls the
existing interrupt handler. If our TSR is NOT currently
running, then it checks for the magic pop keystrokes. If
the TSR is already running, then we do not want to
activate it again, so we ignore keystroke checking when
the TSR is already alive and on-screen.
Several bytes in low memory contain keyboard status
information. By checking the values in these bytes,
various "not normal" key combinations can be detected.
As implemented here, the TSR is made active by pressing
the left Alt key, plus the SysRq key (Print Screen on my
PC). You can change these keystrokes to something else.
}
const
CallTSRMask = 6;
{ ACTIVATE TSR = left Alt key + SysRq key }
var
ScanCode: byte absolute $40:$18;
{ One of the keyboard status bytes. }
begin
{ Call existing keyboard interrupt handler. }
asm
PUSHF
CALL PINT09
end;
if not TSRInUse then
if (ScanCode and CallTSRMask) = CallTSRMask then
begin
{ The TSR has been activated. }
TSRInUse := True;
{ Set to TRUE to prevent reactivation of this TSR. }
if (PInDosFlag^ = 0) then
begin
{ If in "Safe" DOS area, then pop up now. }
MadeActive := False; { So INT $28 won't call us. }
RunPopUp;
TSRInUse := False;
end
else
MadeActive := True;
{ o/w, set flag let INT 28 call us when ok. }
end;
end; {KeyboardInt}
procedure DoUnInstall ( var Removed: Boolean ); forward;
{ These are "local" to OurInt12; it is safer to store them in the
TSR's global data area than to place them on the stack as
local variables. }
var
IdStr : ^String8;
MessageNum : Integer;
procedure OurInt12
(_AX, BX, CX, DX, SI, DI, DS, ES, BP:Word);
interrupt;
{ INT 12H }
{ Intercepts INT 12H calls. If the ES:BX register
just happens to point to IdStr1, then the command-line
TSR program just called in. If this is the case, the
other registers could be used to pass a message to the
running TSR. Here, it's used by the running TSR to return
a pointer to another string, confirming that the TSR is
indeed running.
}
var
DeInstallOk: Boolean;
begin
IdStr := Ptr( ES, BX );
{ Check to see if ES:BX points to the }
{ magic ID string }
If IdStr^ = IdStr1 then
MessageNum := CX
else
begin
MessageNum := 0;
{ No message rcvd; this is normal DOS call. }
asm
pushf
call PInt12
mov _AX, ax
{ AX returns the memory size value. }
end;
end;
if MessageNum > 0 then
{ Process a message directed to this TSR. }
begin
case MessageNum of
1: begin
{ Returns pointer to IdStr2 indicating this
TSR is here. }
ES := Seg(IdStr2);
BX := Ofs(IdStr2);
end;
2: begin { Performs request to uninstall the TSR. }
DoUnInstall (DeInstallOk);
if DeInstallOk then
CX := 0 { Report success. }
else
CX := 1; { Report failure. }
end;
end;
end;
end; {OurInt12}
procedure DoUnInstall ( var Removed: Boolean );
begin
{ See if any TSRs have loaded in memory after us.
If another TSR has loaded after us, then we really
cannot safely terminate this TSR. Why? Because when they
terminate, they may reset their interrupts to point back
to this TSR. And if this TSR is no longer in memory,
uh-oh... }
Removed := True;
GetIntVec( $28, TempPtr );
if TempPtr <> @BackgroundInt then
Removed := False;
GetIntVec( $12, TempPtr );
If TempPtr <> @OurInt12 then
Removed := False;
GetIntVec( $09, TempPtr );
if TempPtr <> @KeyBoardInt then
Removed := False;
if Removed then
begin
{ Restore interrupts }
SetIntVec( $28, PInt28 );
SetIntVec( $12, PInt12 );
SetIntVec( $09, PInt09 );
{ Free up memory allocated to this program using
INT 21 Func=49H "Release memory". }
CPURegisters.AH := $49;
CPURegisters.ES := PrefixSeg;{ Current program's PSP }
Intr( $21, CPURegisters );
end;
end; {DoUnInstall}
procedure InstallTSR (var AlreadyInstalled: Boolean );
{ Installs all interrupt handlers for this TSR. }
var
PSPPtr : ^Word;
IdStr : ^String8;
begin
{ Check to see if the TSR is already running by
executing INT 12 with ES:BX pointing to IdStr1. If,
after calling INT 12, ES:BX points to IdStr2, then the
TSR is running since it must have intercepted INT 12 and
set ES:BX to those return values. }
with CPURegisters do
begin
ES := Seg(IdStr1);
BX := Ofs(IdStr1);
CX := 1;
Intr( $12, CPURegisters );
IdStr := Ptr( ES, BX );
if IdStr^ = IdStr2 then
begin
AlreadyInstalled := True;
Exit;
end;
{ TSR hasn't been installed, so install our
INT 12 driver. }
asm
cli
end;
GetIntVec( $12, PInt12 );
SetIntVec( $12, @OurInt12 );
asm
sti
end;
end;
AlreadyInstalled := False;
MadeActive := False;
DiskInUse := 0;
{ Check to see if TSR is already installed. }
TSRInUse := False;
{ Deallocate DOS Environment block if not needed. }
{ Comment out this code if you need to access
environment strings. }
PSPPtr := Ptr( PrefixSeg, $2C );
CPURegisters.AX := $4900;
CPURegisters.ES := PSPPtr^;
Intr( $21, CPURegisters);
asm
cli
end;
{ Save and set INT 28H, background process interrupt. }
GetIntVec( $28, PInt28 );
SetIntVec( $28, @BackgroundInt );
{ Save and set INT 09H, the keyboard interrupt handler. }
GetIntVec( $09, PInt09 );
SetIntVec( $09, @KeyboardInt );
asm
sti
end;
{ Initialize pointer to DOS's InDos flag. }
{ Uses INT 21H Function 34H to retrieve a pointer to the
InDos flag. The result is returned in the ES:BX
registers. }
CPURegisters.AH := $34;
Intr( $21, CPURegisters );
PInDosFlag := Ptr( CPURegisters.ES, CPURegisters.BX );
asm
CLI
end;
{ Save our SS:SP for later use. }
OurSS := SSeg;
OurSP := SPtr;
asm
STI
end;
end; {InstallTSR}
var
InstallError: Boolean;
begin {program}
InstallTSR( InstallError );
if InstallError then
begin
{ This means that the TSR is already running.
In that case, see if the command line requests
an uninstall. }
if (ParamStr(1) = '/u') or (ParamStr(1) = '/U') then
{ Send an Uninstall message to the TSR }
with CPURegisters do
begin
ES := Seg(IdStr1);
BX := Ofs(IdStr1);
CX := 2;
Intr( $12, CPURegisters );
if CX = 0 then
Writeln('TSR is now uninstalled.')
else
Writeln('Unable to uninstall.');
Exit;
end
else
Writeln('!!!TSR is already installed!!!');
end
else
begin
Writeln('TSR is now resident.');
Keep( 0 );
{ Exit to DOS, leaving program memory-resident. }
end;
end. {program}
[Back to TSR SWAG index] [Back to Main SWAG index] [Original]