19.3 Example of a Simple TSR: ALARM

This section presents a simple alarm clock TSR that demonstrates some of the material covered so far. The program accepts an argument from the command line that specifies the alarm setting in military form, such as 1635 for 4:35 P.M. For the sake of simplicity, the argument must consist of four digits, including leading zeros. To set the alarm at 7:45 A.M., for example, enter:

ALARM 0745

The installation section of the program begins with the Install procedure. Install computes the number of five-second intervals that must elapse before the alarm sounds and stores this number in the word CountDown. The procedure then obtains the vector for Interrupt 08 (timer) through Interrupt 21h Function 35h and stores it in the far pointer OldTimer. Interrupt 21h Function 25h replaces the vector with the far address of the new timer handler NewTimer. Once installed, the new timer handler executes at every timer interrupt. These interrupts occur 18.2 times per second or 91 times every five seconds.

Each time it executes, NewTimer subtracts one from a secondary counter called Tick91. By counting 91 timer ticks, Tick91 accurately measures a period of five seconds. When Tick91 reaches zero, it's reset to 91 and CountDown is decremented by one. When CountDown reaches zero, the alarm sounds.

;* ALARM.ASM - A simple memory-resident program that beeps the speaker

;* at a prearranged time. Can be loaded more than once for multiple

;* alarm settings. During installation, ALARM establishes a handler

;* for the timer interrupt (interrupt 08). It then terminates through

;* the terminate-and-stay-resident function (function 31h). After the

;* alarm sounds, the resident portion of the program retires by setting

;* a flag that prevents further processing in the handler.

;*

;* NOTE: You must assemble this program as a .COM file, either as a PWB

;* build option or with the ML /AT option.

.MODEL tiny, pascal, os_dos

.STACK

.CODE

ORG 5Dh ; Location of time argument in PSP,

CountDown LABEL WORD ; converted to number of 5-second

; intervals to elapse

.STARTUP

jmp Install ; Jump over data and resident code

; Data must be in code segment so it won't be thrown away with Install code.

OldTimer DWORD ? ; Address of original timer routine

tick_91 BYTE 91 ; Counts 91 clock ticks (5 seconds)

TimerActiveFlag BYTE 0 ; Active flag for timer handler

;* NewTimer - Handler routine for timer interrupt (interrupt 08).

;* Decrements CountDown every 5 seconds. No other action is taken

;* until CountDown reaches 0, at which time the speaker sounds.

NewTimer PROC FAR

.IF cs:TimerActiveFlag != 0 ; If timer busy or retired:

jmp cs:OldTimer ; Jump to original timer routine

.ENDIF

inc cs:TimerActiveFlag ; Set active flag

pushf ; Simulate interrupt by pushing flags,

call cs:OldTimer ; then far-calling original routine

sti ; Enable interrupts

push ds ; Preserve DS register

push cs ; Point DS to current segment for

pop ds ; further memory access

dec tick_91 ; Count down for 91 ticks

.IF zero? ; If 91 ticks have elapsed:

mov tick_91, 91 ; Reset secondary counter and

dec CountDown ; subtract one 5-second interval

.IF zero? ; If CountDown drained:

call Sound ; Sound speaker

inc TimerActiveFlag ; Alarm has sounded, set flag

.ENDIF

.ENDIF

dec TimerActiveFlag ; Decrement active flag

pop ds ; Recover DS

iret ; Return from interrupt handler

NewTimer ENDP

;* Sound - Sounds speaker with the following tone and duration:

BEEP_TONE EQU 440 ; Beep tone in hertz

BEEP_DURATION EQU 6 ; Number of clocks during beep,

; where 18 clocks = approx 1 second

Sound PROC USES ax bx cx dx es ; Save registers used in this routine

mov al, 0B6h ; Initialize channel 2 of

out 43h, al ; timer chip

mov dx, 12h ; Divide 1,193,180 hertz

mov ax, 34DCh ; (clock frequency) by

mov bx, BEEP_TONE ; desired frequency

div bx ; Result is timer clock count

out 42h, al ; Low byte of count to timer

mov al, ah

out 42h, al ; High byte of count to timer

in al, 61h ; Read value from port 61h

or al, 3 ; Set first two bits

out 61h, al ; Turn speaker on

; Pause for specified number of clock ticks

mov dx, BEEP_DURATION ; Beep duration in clock ticks

sub cx, cx ; CX:DX = tick count for pause

mov es, cx ; Point ES to low memory data

add dx, es:[46Ch] ; Add current tick count to CX:DX

adc cx, es:[46Eh] ; Result is target count in CX:DX

.REPEAT

mov bx, es:[46Ch] ; Now repeatedly poll clock

mov ax, es:[46Eh] ; count until the target

sub bx, dx ; time is reached

sbb ax, cx

.UNTIL !carry?

in al, 61h ; When time elapses, get port value

xor al, 3 ; Kill bits 0-1 to turn

out 61h, al ; speaker off

ret

Sound ENDP

;* Install - Converts ASCII argument to valid binary number, replaces

;* NewTimer as the interrupt handler for the timer, then makes program

;* memory-resident by exiting through function 31h.

;*

;* This procedure marks the end of the TSR's resident section and the

;* beginning of the installation section. When ALARM terminates through

;* function 31h, the above code and data remain resident in memory. The

;* memory occupied by the following code is returned to DOS.

Install PROC

; Time argument is in hhmm military format. Converts ASCII digits to

; number of minutes since midnight, then converts current time to number

; of minutes since midnight. Difference is number of minutes to elapse

; until alarm sounds. Converts to seconds-to-elapse, divides by 5 seconds,

; and stores result in word CountDown.

DEFAULT_TIME EQU 3600 ; Default alarm setting = 1 hour

; (in seconds) from present time

mov ax, DEFAULT_TIME

cwd ; DX:AX = default time in seconds

.IF BYTE PTR CountDown != ' ' ; If not blank argument:

xor CountDown[0], '00' ; Convert 4 bytes of ASCII

xor CountDown[2], '00' ; argument to binary

mov al, 10 ; Multiply 1st hour digit by 10

mul BYTE PTR CountDown[0] ; and add to 2nd hour digit

add al, BYTE PTR CountDown[1]

mov bh, al ; BH = hour for alarm to go off

mov al, 10 ; Repeat procedure for minutes

mul BYTE PTR CountDown[2] ; Multiply 1st minute digit by 10

add al, BYTE PTR CountDown[3] ; and add to 2nd minute digit

mov bl, al ; BL = minute for alarm to go off

mov ah, 2Ch ; Request function 2Ch

int 21h ; Get Time (CX = current hour/min)

mov dl, dh

sub dh, dh

push dx ; Save DX = current seconds

mov al, 60 ; Multiply current hour by 60

mul ch ; to convert to minutes

sub ch, ch

add cx, ax ; Add current minutes to result

; CX = minutes since midnight

mov al, 60 ; Multiply alarm hour by 60

mul bh ; to convert to minutes

sub bh, bh

add ax, bx ; AX = number of minutes since

; midnight for alarm setting

sub ax, cx ; AX = time in minutes to elapse

; before alarm sounds

.IF carry? ; If alarm time is tomorrow:

add ax, 24 * 60 ; Add minutes in a day

.ENDIF

mov bx, 60

mul bx ; DX:AX = minutes-to-elapse-times-60

pop bx ; Recover current seconds

sub ax, bx ; DX:AX = seconds to elapse before

sbb dx, 0 ; alarm activates

.IF carry? ; If negative:

mov ax, 5 ; Assume 5 seconds

cwd

.ENDIF

.ENDIF

mov bx, 5 ; Divide result by 5 seconds

div bx ; AX = number of 5-second intervals

mov CountDown, ax ; to elapse before alarm sounds

mov ax, 3508h ; Request function 35h

int 21h ; Get Vector for timer (interrupt 08)

mov WORD PTR OldTimer[0], bx ; Store address of original

mov WORD PTR OldTimer[2], es ; timer interrupt

mov ax, 2508h ; Request function 25h

mov dx, OFFSET NewTimer ; DS:DX points to new timer handler

int 21h ; Set Vector with address of NewTimer

mov dx, OFFSET Install ; DX = bytes in resident section

mov cl, 4

shr dx, cl ; Convert to number of paragraphs

inc dx ; plus one

mov ax, 3100h ; Request function 31h, error code=0

int 21h ; Terminate-and-stay-resident

Install ENDP

END

Note the following points about ALARM:

The constant BEEP_TONE specifies the alarm tone. Practical values for the tone range from approximately 100 to 4,000 hertz.

The Install procedure marks the beginning of the installation section of the program. Execution begins here when ALARM.COM is loaded. A TSR generally places its installation code after the resident section. This allows the TSR to include the installation code and data and to return memory to DOS when the program terminates. Since the installation section executes only once, the TSR can discard it after becoming resident.

You can install ALARM any number of times in quick succession, each time with a new alarm setting. The timer handler does not restore the original timer vector after the alarm sounds. In effect, the multiple installations are daisy-chained in memory. The address in OldTimer for one installation is the address of NewTimer in the preceding installation.

Until a system reboot, NewTimer remains in place as the Interrupt 08 handler, even after the alarm sounds. To save unnecessary activity, the byte TimerActiveFlag remains set after the alarm sounds. This forces an immediate jump to the original handler for all subsequent executions of NewTimer.

NewTimer and Sound alter registers DS, AX, BX, CX, DX, and ES. To preserve the original values in these registers, the procedures first push them onto the stack and then restore the original values before exiting. This ensures that the process interrupted by NewTimer continues with valid registers after NewTimer returns.

ALARM requires little stack space. It assumes that the current stack is adequate and makes no attempt to set up a new one. More sophisticated TSRs, however, should as a matter of course provide their own stacks to ensure adequate stack depth. The example program presented in Section 19.8 demonstrates this safety measure.