; MHEART.ASM (MHEART.EXE) ; Date: 04/17/99 ; A public domain program by: ; James "Jim" Webster ; Lafollette, Tennessee (USA) ; jwebste3@bellsouth.net ; ; Music plays while a heart moves across screen. ; User may abort program early by pressing ESC. ; ; This program was a collaboration between myself and ; a computer science student, in Canada, that had asked ; for my assistance in assembly language programming. ; This program does not directly call any interrupts. ; Though the size of this file (25,007 bytes) makes this ; appear to be a large program, the size of the compiled ; program MHEART.EXE is only 1,755 bytes. And 512 bytes of ; that is taken up by the .EXE header. The majority of ; this file consists of data reference tables and the like. ; ; To Compile: MASM MHEART; ; LINK /CP:1 MHEART; ; ; The /CP:1 switch will initialize the .EXE header so that ; when the program is loaded into memory to run, all ; conventional memory that is above and beyond what is ; required to run the program will be freed. data segment ;Note: SCORE*2 is a 0-based pointer into NOTES (13d*2 = highest value) ; and SCORE (*1) is a 0-based pointer into LENGTH_OF_NOTE. score db 5,1,4,1,5,1,6,1,7,1,6,1,7,1,8,1,9,2,9,1,8,1,9,1,10,1,9,1,9,1 db 10,1,11,1,12,1,13,1,12,1,11,1,12,1,10,1,9,1,10,1,9,1,8,1,9,2,9,2 db 11,1,10,1,9,1,8,1,9,1,10,1,9,1,8,1,9,1,8,1,7,1,6,1,7,1,6,1,5,1,4,1 db 5,1,6,1,5,1,4,1,5,1,3,1,2,1,2,1,3,1,4,1,5,1,6,2,6,2 db 5,1,4,1,5,1,6,1,7,1,6,1,5,1,4,1,5,1,4,1,3,1,5,1,4,1,3,1,2,1,1,1 db 2,1,3,1,4,1,5,1,6,1,7,1,8,1,9,1,9,1,10,1,9,1,8,1,9,2,9,2 db 9,1,8,1,7,1,6,1,6,2,7,1,7,1,6,1,7,1,8,1,7,1,8,1,9,2 db 9,1,8,1,7,1,6,1,7,1,6,1,5,1,4,1,5,1,6,1,5,1,5,1,4,1 db 5,1,4,1,3,1,2,1,0,0 ;Note: NOTES = counter for the 8253/8254 System Timer latch = ; 1,190,000 divided by the "cycles per second" desired. notes dw 0,2596h,2394h,1fb5h,1c3fh,1aa2h,17c8h dw 152fh,12dfh,11cah,0fdah,0e20h,0d5bh,0be4h ;Note: LENGTH_OF_NOTE = duration of NOTES. length_of_note db 1,2,4,8,16,32 ;Must have 14 entries (0-13d) because db 1,2,4,8,16,32 ; of the pointer variable SCORE! db 1,2 data ends stack segment stack dw 100h dup(?) stack ends ;******************************************* main segment assume cs:main, ss:stack, ds:data ;******************************************* ;-------------------------------------------------------------------------- ; This procedure is the starting point of this program. ;-------------------------------------------------------------------------- drive proc far push ds mov ax,0 push ax ;Top of stack = Seg:Offs DS:0000 of the PSP ; (Program Segment Prefix), which contains ; an INT 20h pointer. call init_video ;Determine the correct Video Segment. call intset_1c ;Hook INT 1Ch with MYTIMER. call intset_09 ;Hook INT 09h with MYSCANKEY. mov cx,2 ;CX = loop count used by MUSIC. call music ;Play a tune. call restore_09 ;Restore the old INT 09h. call restore_1c ;Restore the old INT 1Ch. ret ;Return FAR to INT 20h (Terminate Program). drive endp ;-------------------------------------------------------------------------- ; Description of Ports 40h, 41h, 42h, and 42h ; of the 8253/8254 System Timer chip. ; ; ; Port 40h (Timer 0) (This is a write-only port.) ; Port 41h (Timer 1) (This is a write-only port.) ; Port 42h (Timer 2) (This is a write-only port.) ; ; Port 43h (Control Register) (This is a read and write port.) ; The value of the Control Word Byte (defined below) that is written ; to Port 43h affects the operation of Port 40h, 41h, and 42h. ; ; MSB = Most Significant Bit ; LSB = Least Significant Bit ; ; MSB ---> 0000-0000 <--- LSB ; 7654 3210 <--- Bit number. ; ; Bit(s) Description Settings ; ------ ------------------ -------------------------------------------- ; 0 Count Type 0 = Binary ; 1 = BCD (Binary Coded Decimal) ; 1-3 Mode Number 000 = Interrupt on Terminal Count ; 001 = Programmable One-Shot ; 010 = Rate Generator ; 011 = Square Wave Generator ; 100 = Software Triggered Strobe ; 101 = Hardware Triggered Strobe ; 4-5 Latch, Read Format 00 = Latch Current Count ; 01 = Read Low Byte (no latching) ; 10 = Read High Byte (no latching) ; 11 = Read Low Byte (LSB), then High Byte (MSB) ; 6-7 Timer Number 00 = Timer 0 ; 01 = Timer 1 ; 10 = Timer 2 ;-------------------------------------------------------------------------- ;-------------------------------------------------------------------------- ; Contents of Ports 60h, 61h, and 62h ; of the 8255 PPI (Programmable Peripheral Interface) chip. ; ; ; Port 60h (Port A) (This is a read-only port.) ; If bit #7 of Port B (61h) = 0, contents = Scan Code of key press. ; If bit #7 of Port B (61h) = 1, contents = the below. ; ; MSB = Most Significant Bit ; LSB = Least Significant Bit ; ; MSB ---> 0000-0000 <--- LSB ; 7654 3210 <--- Bit number. ; ; Bit(s) Description Settings ; ------ ---------------------------- ---------------------------------- ; 0 Floppy Drive Installed 1 = Yes ; 1 Unused ; 2-3 Banks of RAM on System Board ; 4-5 Video Mode 01 = Color (40 rows x 25 columns) ; 10 = Color (80 rows x 25 columns) ; 11 = Monochrome (80 row x 25 col) ; 6-7 Number of Disk Drives 00 = 1 ; 01 = 2 ; 10 = 3 ; 11 = 4 ; ; ; Port 61h (Port B) (This is a read and write port.) ; ; MSB ---> 0000-0000 <--- LSB ; 7654 3210 <--- Bit number. ; ; Bit(s) Description Settings ; ------ ---------------------------- ---------------------------------- ; 0 Controls Gate of Channel 2 ; of 8253/8254 System Timer 1 = Enable Channel 2 ; 1 Output to PC Speaker 1 = Yes ; 2 PC Select Port C 1 = See Port C (62h) below. ; 3 XT Select Port C 1 = See Port C (62h) below. ; 4 RAM refresh 0 = Enable RAM ; 5 Unused ; 6 Keyboard Clock Signal 1 = Enable ; 7 Port A (60h) control bit 0 = Return Scan Code of key press ; 1 = Return System Configuration ; ; ; Port 62h (Port C) (This is a read-only port.) ; ; MSB ---> 0000-0000 <--- LSB ; 7654 3210 <--- Bit number. ; ; Bit(s) Description Settings ; ------ ---------------------------- ---------------------------------- ; (If bit 2 or 3 of Port B (61h) = 0. ) ; 0-3 Configuration Switches 1-4 ; (If bit 2 of Port B (61h) = 1. ) ; 0-3 Configuration Switches 5-8 ; (If bit 3 of Port B (61h) = 1. ) ; 0-1 Video Mode 01 = Color (40 rows x 25 columns) ; 10 = Color (80 rows x 25 columns) ; 11 = Monochrome (80 row x 25 col) ; 2-3 Number of Disk Drives 00 = 1 ; 01 = 2 ; 10 = 3 ; 11 = 4 ; (The remaining bits are same for all.) ; 4 PC Data from Cassette ; 5 Output to Channel 2 of ; 8253/8254 System Timer ; 6 Unused ; 7 Parity Error Status 1 = Parity Error occurred ;-------------------------------------------------------------------------- ;-------------------------------------------------------------------------- ; This procedure turns on the PC speaker. ; ; Caller(s): MUSIC ;-------------------------------------------------------------------------- pc_spkr_on proc ;Connect 8253 and 8255 to the PC speaker. push ax in al,61h ;Read in current value of Port B of 8255 PPI. or al,3 ;(0000-0011) Set bits 0 and 1. out 61h,al ;Send new value to Port B of 8255 PPI. mov al,0b6h ;AL= Control Word Byte (1011-0110). ; 7654 3210 <--- LSB ; Bits Value Meaning ; ---- ----- ---------------------------- ; 0 = 0 = Binary (raw) mode. ; 1-3 = 011 = Square Wave Generator mode. ; 4-5 = 11 = Read LSB, then MSB. ; 6-7 = 10 = We're Initialing Timer 2. out 43h,al ;Send Control Word Byte to Command Register pop ax ; of 8253/8254 System Timer. ret pc_spkr_on endp ;-------------------------------------------------------------------------- ; This procedure turns off the PC speaker. ; ; Caller(s): MUSIC ;-------------------------------------------------------------------------- pc_spkr_off proc ;Disconnect the PC speaker. in al,61h ;Read in current value of Port B of 8255 PPI. and al,0fch ;(1111-1100) Clear bits 0 and 1. out 61h,al ;Send new value to Port B of 8255 PPI. ret ; This also disables 8253/8254 System Timer. pc_spkr_off endp ;-------------------------------------------------------------------------- ; This procedure kills time by doing preset loops. ; ; Caller(s): TIME ;-------------------------------------------------------------------------- delay proc push cx push dx mov dx,030h ;DX = outer loop count. repeat1:mov cx,0ffffh ;CX = inner loop count. repeat2:nop dec cx ;Decrement the inner loop count. jne repeat2 ;If CX <> 0, continue inner loop. dec dx ;Else, decrement the outer loop count. jne repeat1 ;If DX <> 0, continue outer loop. pop dx ;Else, exit procedure. pop cx ret delay endp ;-------------------------------------------------------------------------- ; This procedure plays sound through the PC speaker. ; ; Caller(s): DRIVE ; Reads: KEYCODE, NOTES, LENGTH_OF_NOTE ;-------------------------------------------------------------------------- music proc jmp short start ;Jump to begin. abort_music: call pc_spkr_off ;Turn off the PC speaker. ret ;And exit procedure. start: call pc_spkr_on ;Turn on the PC speaker. mov ax,data mov ds,ax ;DS = the programs DATA Segment. mov si,0 ;DS:SI = address of SCORE. play: cmp cs:keycode,01h ;ESC key pressed? je abort_music ;If so, abort program. mov al,[si] ;AL = one SCORE (pointer). add al,al ;AL = AL * 2. Create a word pointer. mov ah,0 ;AH = 0. mov di,ax ;DI = offset into NOTES. mov dx,notes[di] ;DX = counter for the 8253/8254 latch = ; 1,190,000/cycles per second desired. cmp dx,0 ;End of notes ? je oncemore ;If DX = 0, yes. Jump. mov al,dl ;AL = LSB. Must send this first. out 42h,al ;Send LSB to Timer 2 of 8253/8254 Timer. mov al,dh ;AL = MSB. Must send this last. out 42h,al ;Send MSB to Timer 2 of 8253/8254 Timer. inc si ;Increment pointer into SCORE. mov al,[si] ;AL = one SCORE (pointer). mov ah,0 ;AH = 0. mov di,ax ;DI = pointer into LENGTH_OF_NOTE. mov al,length_of_note[di] ;AX = outer loop count for TIME = ; duration/length of NOTES. cmp cs:keycode,01h ;ESC key pressed? je abort_music ;If so, abort program. call time ;Kill some time. inc si ;Increment pointer into SCORE. jmp play ;And jump to continue the music. oncemore: call pc_spkr_off ;Turn off the PC speaker. cmp cs:keycode,01h ;ESC key pressed? je end_music ;If so, abort program. mov ax,32 ;AX = outer loop count for TIME. call time ;Kill some time. dec cx ;Decrement loop count of MUSIC. jne start ;If CX <> 0, do it again. end_music: ;Else, exit procedure. ret music endp ;-------------------------------------------------------------------------- ; This procedure kills AX amount of time. ; ; Caller(s): MUSIC ; On entry: AX = the outer loop count. ;-------------------------------------------------------------------------- time proc push ax again: dec ax ;Decrement the outer loop count. jl back ;If AX = 0, exit. call delay ;Else, do some dummy loops. jmp again ;And continue the outer loop. back: pop ax ret time endp ;-------------------------------------------------------------------------- ; This procedure is hooked into INT 1Ch on program entry and then unhooked ; on program exit. Each time that INT 08h (the System Timer Interrupt - ; IRQ 0) is invoked, it in turn calls INT 1Ch. Which, by default, is just ; an IRET instruction. This occurs roughly 18.2 times per second. ; INT 1Ch is commonly hooked by DOS-based TSR (Terminate and Stay Resident) ; programs. ; ; Reads: BIOSINT1C, COUNTER ; Writes: COUNTER ;-------------------------------------------------------------------------- biosint1c label dword ;Can now access as a word or double word. lowaddr_1c dw 0 ;Offset address of old INT 1Ch. highaddr_1c dw 0 ;Segment address of old INT 1Ch. counter dw 0 ;Loop counter for DRAW1. mytimer proc push cx pushf ;Simulate INT by pushing flags on stack. call cs:biosint1c ;Call the old INT 1Ch procedure. mov cx,cs:counter ;CX = previous count variable. inc cx ;CX = CX + 1. Increment the count. cmp cx,18 ;Has 1 second elapsed ? jb notyet ;If CX <> 18, no. Jump to exit. call draw1 ;Else, move the Heart character (03h). mov cx,0 ;Reset count variable to 0. notyet: mov cs:counter,cx ;Store the new count variable. pop cx iret mytimer endp ;-------------------------------------------------------------------------- ; This procedure draws the star character (03h) to the screen. ; ; Note: Each character in the Video Segment occupies 2 bytes (a word). ; The 1st byte is the character itself. ; The 2nd byte is the character attribute (aka Foreground Color). ; Because the Intel chip swaps the byte values during a "read from" ; or "write to" memory, the AX register value is also swapped. So ; that when the value in AX is written to memory, it is stored ; correctly. Aka, 0720 is written to memory as 2007. ; ; This is also why (for example) BIOSINT1C is initialed with the ; offset address first. If the actual address were 1234:5678 ; (in Segment:Offset format), because of the Intel architecture, ; this value would be stored in memory as: 78 56 34 12. ; ; Caller(s): MYTIMER ; Reads: SCREEN_SEG, POSITION ; Writes: POSITION ;-------------------------------------------------------------------------- position dw 0fffeh ;Offset address in Video segment. draw1 proc push ax push bx push es mov ax,cs:screen_seg mov es,ax ;ES = Video segment. mov bx,cs:position ;BX = current offset in Video segement. mov ax,0720h ;AH = white attribute. AL = Space character. mov es:[bx],ax ;Write AX to screen. Overwrite old Heart. add bx,2 ;Increment offset pointer in Video segment. mov ax,0703h ;AH = white attribute. AL = Heart character. mov es:[bx],ax ;Write AX to screen. Write new Heart. mov cs:position,bx ;Store the new offset. pop es pop bx pop ax ret draw1 endp ;-------------------------------------------------------------------------- ; This procedure hooks INT 1Ch with the procedure MYTIMER. ; ; Caller(s): DRIVE ; Reads: MYTIMER ; Writes: LOWADDR_1C, HIGHADDR_1C ;-------------------------------------------------------------------------- intset_1c proc push ax push bx push ds mov ax,0 mov ds,ax ;DS = 0. mov bx,0070h ;DS:BX = address of INT 1Ch. mov ax,[bx] ;AX = offset address of current INT 1Ch. mov cs:lowaddr_1c,ax ;Store it. mov ax,[bx+2] ;AX = segment address of current INT 1Ch. mov cs:highaddr_1c,ax ;Store it. cli ;Disable interrupts. mov ax,offset mytimer mov [bx],ax ;Store offset address of MYTIMER at INT 1Ch. mov ax,cs mov [bx+2],ax ;Store segment address of MYTIMER at INT 1Ch. sti ;Enable interrupts. pop ds pop bx pop ax ret intset_1c endp ;-------------------------------------------------------------------------- ; This procedure restores the original INT 1Ch. ; ; Caller(s): DRIVE ; Reads: LOWADDR_1C, HIGHADDR_1C ;-------------------------------------------------------------------------- restore_1c proc push ax push bx push ds mov ax,0 mov ds,ax ;DS = 0. mov bx,0070h ;DS:BX = address of INT 1Ch. cli ;Disable interrupts. mov ax,cs:lowaddr_1c mov [bx],ax ;Restore the original offset address. mov ax,cs:highaddr_1c mov [bx+2],ax ;Restore the original segment address. sti ;Enable interrupts. pop ds pop bx pop ax ret restore_1c endp ;-------------------------------------------------------------------------- ; This procedure is hooked into INT 09h on program entry and then unhooked ; on program exit. INT 09h is known as the Keyboard Hardware Interrupt. ; And is invoked each time that a key is pressed or released. ; ; Reads: BIOSINT09 ; Writes: KEYCODE ;-------------------------------------------------------------------------- biosint09 label dword ;Can now access as a word or double word. lowaddr_09 dw 0 ;Offset address of old INT 09h. highaddr_09 dw 0 ;Segment address of old INT 09h. keycode db 0 ;Scan Code of keypress. ESC = 01h. myscankey proc push ax cli ;Disable interrupts. in al,60h ;Fetch Scan Code character from Port A ; of 8255 PPI. mov cs:keycode,al ;Store the Scan Code character. sti ;Enable interrupts. pop ax pushf ;Simulate INT by pushing flags on stack. call cs:biosint09 ;Call the old INT 09h procedure. iret myscankey endp ;-------------------------------------------------------------------------- ; This procedure hooks INT 09h with the procedure MYSCANKEY. ; ; Caller(s): DRIVE ; Reads: MYSCANKEY ; Writes: LOWADDR_09, HIGHADDR_09 ;-------------------------------------------------------------------------- intset_09 proc push ax push bx push ds mov ax,0 mov ds,ax ;DS = 0. mov bx,0024h ;DS:BX = address of INT 09h. mov ax,[bx] ;AX = offset address of current INT 09h. mov cs:lowaddr_09,ax ;Store it. mov ax,[bx+2] ;AX = segment address of current INT 09h. mov cs:highaddr_09,ax ;Store it. cli ;Disable interrupts. mov ax,offset myscankey mov [bx],ax ;Store offset addr of MYSCANKEY at INT 09h. mov ax,cs mov [bx+2],ax ;Store segment addr of MYSCANKEY at INT 09h. sti ;Enable interrupts. pop ds pop bx pop ax ret intset_09 endp ;-------------------------------------------------------------------------- ; This procedure restores the original INT 09h. ; ; Caller(s): DRIVE ; Reads: LOWADDR_09, HIGHADDR_09 ;-------------------------------------------------------------------------- restore_09 proc push ax push bx push ds mov ax,0 mov ds,ax ;DS = 0. mov bx,0024h ;DS:BX = address of INT 09h. cli ;Disable interrupts. mov ax,cs:lowaddr_09 mov [bx],ax ;Restore the original offset address. mov ax,cs:highaddr_09 mov [bx+2],ax ;Restore the original segment address. sti ;Enable interrupts. pop ds pop bx pop ax ret restore_09 endp ;-------------------------------------------------------------------------- ; This procedure determines the correct Video Segment to use by examining ; the Equipment Flag field (0040:0010) in the BIOS Data Area. ; ; Caller(s): DRIVE ; Writes: SCREEN_SEG ;-------------------------------------------------------------------------- ; Please note that this field has been around for a long time ; and that not all of the bit settings can be relied upon to ; indicate what I have noted below. ; ; Equipment Flag bit settings (from 0040:0010 of BIOS Data Area): ; ; MSB = Most Significant Bit ; LSB = Least Significant Bit ; ; MSB ---> 0000-0000-0000-0000 <--- LSB ; 1111 1198 7654 3210 <--- Bit number. ; 5432 10 ; ; Bit(s) Description Settings ; ------ ---------------------------- ---------------------------------- ; 0 Floppy Drive Installed 1 = Yes ; 1 Math Coprocessor Installed 1 = Yes ; 2-3 RAM on System Board 00 = 16k ; 01 = 32k ; 11 = 64k ; 4-5 Video Mode 01 = Color (40 rows x 25 columns) ; 10 = Color (80 rows x 25 columns) ; 11 = Monochrome (80 row x 25 col) ; 6-7 Number of Disk Drives 00 = 1 ; 01 = 2 ; 10 = 3 ; 11 = 4 ; 8 DMA Chip Present 1 = Yes ; 9-11 Number of Serial Ports Reference bits 6-7. ; 12 Game Adapter Installed 1 = Yes ; 13 Internal Modem Installed 1 = Yes ; 14-15 Number of Printers Installed Reference bits 6-7. ;-------------------------------------------------------------------------- screen_seg dw 0b800h ;Segment address of Video Segment. ; Assume a color display. init_video proc push ax push bx push ds mov ax,40h mov ds,ax ;DS = BIOS Data Area. mov bx,10h ;DS:BX = address of Equipment Flag. mov ax,[bx] ;AX = value of Equipment Flag. and al,30h ;(0011-0000) Isolate bits 4 and 5. cmp al,30h ;Monochrome display adapter ? jne exit_init_video ;No, it is color. Exit. mov cs:screen_seg,0b000h ;Else, it is monochrome. Must use B000h. exit_init_video: pop ds pop bx pop ax ret init_video endp main ends end drive ;Tell compiler where the program begins. end