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.