;; 8086 Pie, 8086-based Pi/e ;; ;; Jan 18, 2013 ;; ;; (c) 2013, All Rights Reserved, Egan Ford (egan@sense.net) ;; ;; THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY ;; KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE ;; IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A ;; PARTICULAR PURPOSE. ;; ;; 8086 Pie will compute 1000 decimal digits of Pi or e. ;; ;; All 8086 Pie computation is base 2^16 (65536) and then converted to base 10 ;; for display (technically base 10000, but they both display the same). ;; Arrays of 16-bit words are used to represent multiprecision numbers. In ;; the comments below "array" refers to a big endian multiprecision number. ;; ;; IBM 5160 8088 4.77 MHz time with no output: ~15 seconds ;; ;; C snippets: J.W. Stumpel, (c) May 1991 ;; ;; Assembler: Macro Assembler AS V1.42 ;; ; Assembler: Macro Assembler AS V1.42 cpu 8086 ; Intel 8086 ; Code optimized for 8088 ;; constants (R/O) dec_len = 2000 ; 2000 decimal digits bin_len = 418 ; ceil(2001 / log(65536)) + 2 = 418 ; Duff's Device 4x unrolled loops assume ; bin_len % 4 = 2 quiet = 0 ; do not display results dos = 1 ; use dos calls if dos == 1 exit = 020h ; dos exit dosint = 021h ; dos call int else exit = 067h ; XT server exit coutint = 065h ; XT server cout int endif ;; start of global macros/functions ; Description: Set initial value left of decimal of arg1 to arg2 ; Input: arg1 (16-bit), arg2 (16-bit) ; arg1 = immediate address at start of array ; arg2 = immediate integer to be set left of decimal point ; Registers: AX, CX, DI mset macro arg1, arg2 ; arg1 = arg2 mov di,arg1 ; DI point to arg1 cld ; forward loop sub ax,ax ; AX = 0 mov cx,bin_len ; CX = bin_len (array len) rep stosw ; arg1 now = 0 mov word ptr [arg1],arg2 ; arg1[0] = arg2 endm ; Description: Transfer array (arg2)[0,sizeof(arg2)] to array (arg1) ; Input: arg1 = immediate address/pointer to start of array ; arg2 = immediate address/pointer to start of array ; Registers: CX, SI, DI mcopy macro arg1,arg2 ; arg1 = arg2 cld ; forward copy if substr("arg1",0,1) = "["; [arg1] mov di,word ptr arg1 ; DI points to arg1 else ; arg1 mov di,arg1 ; DI points to arg1 endif if substr("arg2",0,1) = "["; [arg2] mov si,word ptr arg2 ; SI points to arg2 else ; arg2 mov si,arg2 ; SI points to arg2 endif mov cx,bin_len ; length of block (in words) rep movsw ; copy it endm ; Description: Add array arg2 to array arg1 (arg1 = arg1 + arg2) ; Input: arg1 = immediate address/pointer to start of array ; arg2 = immediate address/pointer to start of array ; Calls: add_mp ; Registers: AX, CX, DI, SI madd macro arg1,arg2 ; arg1 = arg1 + arg2 if substr("arg1",0,1) = "["; [arg1] mov di,word ptr arg1 ; DI points to arg1 end add di,(bin_len-1)*2 else ; arg1 mov di,arg1+((bin_len-1)*2) ; DI points to arg1 end endif if substr("arg2",0,1) = "["; [arg2] mov si,word ptr arg2 ; SI points to arg2 end add si,(bin_len-1)*2 else ; arg2 mov si,arg2+((bin_len-1)*2) ; SI points to arg2 end endif call add_mp endm ; Description: Subtract array arg2 from array arg1 (arg1 = arg1 - arg2) ; Input: arg1 = immediate address/pointer to start of array ; arg2 = immediate address/pointer to start of array ; Calls: sub_mp ; Registers: AX, CX, DI, SI msub macro arg1,arg2 ; arg1 = arg1 - arg2 if substr("arg1",0,1) = "["; [arg1] mov di,word ptr arg1 ; DI points to arg1 end add di,(bin_len-1)*2 else ; arg1 mov di,arg1+((bin_len-1)*2) ; DI points to arg1 end endif if substr("arg2",0,1) = "["; [arg2] mov si,word ptr arg2 ; SI points to arg2 end add si,(bin_len-1)*2 else ; arg2 mov si,arg2+((bin_len-1)*2) ; SI points to arg2 end endif call sub_mp endm ; Description: Left shift array (arg1) ; Input: arg1 = immediate address at start of array ; Calls: asl_mp ; Registers: AX, CX, SI masl macro arg1 ; arg1 = arg1 * 2 mov si,arg1+((bin_len-1)*2) ; SI points to arg1 end call asl_mp endm ; Description: Divide (arg1) by a 16-bit quantity (arg1 = arg1 / arg2) ; Input: arg1 = immediate address at start of array ; arg2 = immediate address of divisor ; Calls: div_mp ; Return: If a = 0 (array of zeros), then carry is set, ; otherwise carry is clear. ; Registers: AX, BX, CX, DX, DI mdiv macro arg1,arg2 ; arg1 = arg1 / arg2 mov di,arg1 ; DI points to arg1 mov bx,arg2 ; BX = arg2 (divisor) call div_mp endm ; Description: Compute arctan(1/arg2) and store at (arg1). ; Input: arg1 = immediate address at start of array ; arg2 = immediate integer to be set left of decimal point ; Calls: atan_mp ; Registers: AX, BX, CX, DX, DI, SI, BP matan macro arg1,arg2 ; arg1 = atan(1/arg2) mov si,arg1 ; SI points to arg1 mov ax,arg2 ; AX = arg2 call atan_mp endm ; Description: Convert and print a base 256 number (ptr_a) as base 10/10000. ; Input: arg1 (16-bit) immediate address at start of an array ; Calls: print_mp ; Registers: AX, BX, CX, DX, SI, DI, BP mprint macro arg1 ; print arg1 mov di,arg1 ; DI points to arg1 mov si,arg1+((bin_len-1)*2) ; SI points to arg1 end call print_mp endm ;; start of main code if dos == 1 org 0100h ; COM files start here else org 0h ; XT server start here mov ax,cs ; setup DOS like defaults mov ds,ax ; by setting all segments mov es,ax ; = to CS mov ss,ax mov sp,0 ; put SP at top of segment sti ; enable interrupts for the timer endif main: mov bx,pitext ; bx points to string call print ; print it call ZTimerOn ; start long ztimer call pi ; compute Pi ;call e ; compute e mprint mp_a ; print results call ZTimerOff ; end long ztimer call ZTimerReport ; report timing int exit ; return to DOS or XT Server ;; start of variables pitext: db "2000 DIGITS OF PI = \0"; Change PI to E for, well, e ;; start of subs ; Description: Compute e using the infinite series: ; ; oo ; ===== ; \ 1 ; e = > -- = 1 + 1/1! + 1/2! + 1/3! + 1/4! + ... ; / k! ; ===== ; k = 0 ; ; and save in array mp_a. ; ; Input: None ; ; Output: mp_a = e = 1 + 1/1! + 1/2! + 1/3! + 1/4! + ... ; ; Registers: AX, BX, CX, DX, DI, SI, BP ; ; Calls: mset/set_mp, mdiv/div_mp, madd/add ; ; Globals: n, mp_a, mp_x ; ; C Algorithm: ; ; int n = 1; ; setbig(A, 1, 0); ; setbig(X, 1, 0); ; while(divbig(X, n++)) // while(dividend != 0) ; addbig(A, X); e: mov bp,0 ; n = 0 mset mp_a,1 ; a = 1 mset mp_x,1 ; x = 1 e_loop: inc bp ; n = n + 1 mdiv mp_x,bp ; x / n jc + ; check for no zeros ret ; all zeros, done / madd mp_a,mp_x ; a = a + x jmp e_loop ; Description: Compute pi using the Gregory expansion of Machin's arctan ; formula and save in array mp_a. ; ; pi = 4 * (4 * atan(1/5) - atan(1/239) ) ; ; ; __ / / 1 1 1 \ / 1 1 1 \\ ; || = 4 | 4 | - - ---- + ---- - ... | - | --- - ------ + ------ - ... || ; | | 5 3 5 | | 239 3 5 || ; \ \ 3x5 5x5 / \ 3x239 5x239 // ; ; ; Input: None ; ; Output: mp_a = pi = 4 * (4 * atan(1/5) - atan(1/239)) ; ; Registers: AX, BX, CX, DX, DI, SI, BP ; ; Calls: matan/atan_mp, masl/asl_mp, msub/sub_mp pi: matan mp_a,5 ; a = atan(1/5) masl mp_a ; a = a * 4 masl mp_a matan mp_b,239 ; b = atan(1/239) msub mp_a,mp_b ; a = a - b masl mp_a ; a = a * 4 masl mp_a ret ; Description: Compute arctan(1/N) using the Gregory expansion of Machin's ; arctan formula and save in array (ptr_a). ; ; ; / 1 1 1 \ ; arctan(1/N) = | - - ---- + ---- - ... | ; | N 3 5 | ; \ 3xN 5xN / ; ; ; Input: SI pointer to array ; AX = N ; ; Output: SI pointer to array = arctan(1/N) ; ; Registers: AX, BX, CX, DX, DI, SI, BP ; ; Calls: mset/set_mp, mdiv/div_mp, madd/add_mp, msub/sub_mp, ; mcopy/copy_mp ; ; Globals: mp_x (16-bit), mp_y (16-bit) ; ; C Algorithm: ; ; void atanbig(bignum A, unsigned short x) ; { ; bignum X, Y; ; unsigned short n = 1; ; ; setbig(X, 1, 0); ; divbig(X, x); ; copybig(A, X); ; x *= x; ; while (1) { ; n += 2; ; divbig(X, x); ; copybig(Y, X); ; if (!divbig(Y, n)) // dividend = 0 ; break; ; if (n & 2) ; subbig(A, Y); ; else ; addbig(A, Y); ; } ; } x2: dw 0 ptr_a: dw 0 atan_mp: mov word ptr ptr_a,si ; ptr_a = si mov bp,ax mov bx,ax ; sqr (ax) mul bx mov word ptr [x2],ax mset mp_x,1 ; x = 1 mdiv mp_x,bp ; x /= arg_a mcopy [ptr_a],mp_x ; a = x mov bp,1 ; n = 1 atan_mp_loop: ; main loop inc bp ; n++ inc bp ; n++ mdiv mp_x,word ptr [x2] ; x /= x2 mcopy mp_y,mp_x ; y = x mdiv mp_y,bp ; y /= n jc + ; check for no zeros ret ; all zeros / mov ax,bp ; AX = [n] and ax,010b ; odd/even check on 2nd bit jz + msub [ptr_a],mp_y ; a = a - y jmp atan_mp_loop / madd [ptr_a],mp_y ; a = a + y jmp atan_mp_loop ; Description: Multiprecision addition: a = a + b ; ; Input: DI address to array (a) ; SI address to array (b) ; ; Output: a = a + b ; ; Registers: AX, CX, DI, SI add_mp: std ; reverse direction mov cx,bin_len ; CX = bin_len clc ; clear carry jmp ++ / lodsw ; AX = [SI], SI=SI-2 adc ax,[di] ; AX += [DI] stosw ; [DI] = AX, DI=DI-2 dec cx lodsw ; AX = [SI], SI=SI-2 adc ax,[di] ; AX += [DI] stosw ; [DI] = AX, DI=DI-2 dec cx / lodsw ; AX = [SI], SI=SI-2 adc ax,[di] ; AX += [DI] stosw ; [DI] = AX, DI=DI-2 dec cx lodsw ; AX = [SI], SI=SI-2 adc ax,[di] ; AX += [DI] stosw ; [DI] = AX, DI=DI-2 loop -- ; CX=CX-1, JNZ ret ; Description: Multiprecision subtraction: a = a - b ; ; Input: DI address to array (a) ; SI address to array (b) ; ; Output: a = a - b ; ; Registers: AX, CX, DI, SI sub_mp: std ; reverse direction mov cx,bin_len ; CX = bin_len clc ; clear carry jmp ++ / lodsw ; AX = [SI], SI=SI-2 sbb [di],ax ; [DI] -= AX dec di ; DI-- dec di ; DI-- dec cx ; CX-- lodsw ; AX = [SI], SI=SI-2 sbb [di],ax ; [DI] -= AX dec di ; DI-- dec di ; DI-- dec cx ; CX-- / lodsw ; AX = [SI], SI=SI-2 sbb [di],ax ; [DI] -= AX dec di ; DI-- dec di ; DI-- dec cx ; CX-- lodsw ; AX = [SI], SI=SI-2 sbb [di],ax ; [DI] -= AX dec di ; DI-- dec di ; DI-- loop -- ; CX=CX-1, JNZ ret ; Description: Multiprecision left shift: a = a * 2 ; ; Input: SI address to array (a) ; ; Output: a = a * 2 ; ; Registers: AX, CX, SI asl_mp: mov cx,bin_len ; CX = bin_len clc ; clear carry jmp ++ / rcl word ptr [si],1 ; [SI] *= 2 dec si ; SI-- dec si ; SI-- dec cx ; CX-- rcl word ptr [si],1 ; [SI] *= 2 dec si ; SI-- dec si ; SI-- dec cx ; CX-- / rcl word ptr [si],1 ; [SI] *= 2 dec si ; SI-- dec si ; SI-- dec cx ; CX-- rcl word ptr [si],1 ; [SI] *= 2 dec si ; SI-- dec si ; SI-- loop -- ; CX=CX-1, JNZ ret ; Description: Multiprecision n-bit/16-bit division: a = a / b ; ; Input: DI = address to array (a) ; BX = divisor (b) ; ; Output: a = a / b ; ; Registers: AX, BX, CX, DX, DI ; ; C Algorithm: ; ; short divbig(number, x) ; bignum number; ; unsigned short x; ; { ; dword result; ; short j = 0; ; unsigned short rest = 0; ; ; while (number[j] == 0 && j < MAXSIZE) ; j++; ; if (j == MAXSIZE) ; return (0); ; while (j < MAXSIZE) { ; result.w.lo = number[j]; ; result.w.hi = rest; ; number[j] = result.L / x; ; rest = result.L % x; ; j++; ; } ; return (1); ; } div_mp: ; skip leading zeros cld ; forward check mov cx,bin_len ; CX <- bin_len mov ax,0h ; AL <- 0h repz scasw ; search for non-zero jz ++ ; if all zero jmp to end ; setup for MP division dec di ; rewind back a word dec di ; from zero search inc cx ; rewind CX as well sub dx,dx ; zero out DX (div MSW) ; Duff's device, CX % 4 to find entry point mov ax,cx ; AX <- CX and ax,011b ; mask off last 2 bits and update zero flag jz + ; 00 & 11 = 0; 4, 8, 12, ... shr ax,1 ; 00 => 0,0; 01 => 0,1; 10 => 1,0; 11 => 1,1 (AX,CF) jz d1 ; 01 => 0,1; 1, 5, 9, ... jnc d2 ; 10 => 1,0; 2, 6, 10, ... jmp d3 ; 11 => 1,1; 3, 7, 11, ... ; unroll 4x for speed / mov ax,[di] ; load next 16-bit digit div bx ; div by BX mov [di],ax ; save quotient to back to [DI] inc di ; dec DI twice for inc di ; next word dec cx ; CX = CX - 1 d3: mov ax,[di] ; load next 16-bit digit div bx ; div by BX mov [di],ax ; save quotient to back to [DI] inc di ; dec DI twice for inc di ; next word dec cx ; CX = CX - 1 d2: mov ax,[di] ; load next 16-bit digit div bx ; div by BX mov [di],ax ; save quotient to back to [DI] inc di ; dec DI twice for inc di ; next word dec cx ; CX = CX - 1 d1: mov ax,[di] ; load next 16-bit digit div bx ; div by BX mov [di],ax ; save quotient to back to [DI] inc di ; dec DI twice for inc di ; next word loop - ; CX=CX-1, jump if CX != 0 stc ; set carry if non zero ret / clc ; all zeros, clear carry and ret ; return ; Description: Print an mp array base 10 (base 10000 really) ; ; Input: SI address to array (a) ; DI address at end of array + 2 bytes (1 word) ; (computed by macro) ; ; Output: (a) base 10/10000 out to screen ; ; Registers: AX, BX, CX, DX, SI, DI, BP ; ; Calls: prbcd ptr_di: dw 0 ; SI backup print_mp: ; print left of decimal point mov ax,[di] ; load first digit in AX cmp ax,10 ; if < 10 skip prbcd jc + ; and jmp to / call prbcd ; if > 9 then call prbcd jmp ++ ; and jmp two / / add ax,'0' ; convert to ASCII and call cout ; print to screen / mov al,'.' ; load decimal point and call cout ; print to screen mov word ptr [di],0 ; zero MSW mov bp,dec_len/4 ; setup BP for outer loop mov word ptr [ptr_di],si ; set ptr_di to be end of array (SI) ; main loop, each loop prints 4 digits ; by multiplying mp array by 10000 ; and then printing the result left of ; decimal point, then zeros it / mov di,word ptr [ptr_di] ; reset DI to end of array sub si,si ; SI = 0, SI used for carry mov bx,10000 ; BX = 10000, *10000 for 4 digits mov cx,bin_len ; CX = bin_len (array length) ; multi array x 10000 loop ; loop from LSW to MSW ; 32-bit product = array[i] * 10000 + carry_mp; ; array[i] = product lo ; carry_mp = product hi ; ; C Algorithm: ; ; while (j >= 0) { ; result.L = (long) number[j] * 10000 + carry; ; number[j] = result.w.lo; ; carry = result.w.hi; ; j--; ; } ; Unroll 4x for speed jmp ++ ; Duff device, bin_len = 210, 210 % 4 = 2 / mov ax,[di] ; load into AX, and mult mul bx ; DX:AX = AX * 10000 add ax,si ; AX = AX + mp_carry(SI) adc dx,0 ; Add carry flag stosw ; store LSW, [DI] = AX, DI=DI-2 mov si,dx ; save mp_carry(SI) for next iteration dec cx ; CX=CX-1 mov ax,[di] ; load into AX, and mult mul bx ; DX:AX = AX * 10000 add ax,si ; AX = AX + mp_carry(SI) adc dx,0 ; Add carry flag stosw ; store LSW, [DI] = AX, DI=DI-2 mov si,dx ; save mp_carry(SI) for next iteration dec cx ; CX=CX-1 / mov ax,[di] ; load into AX, and mult mul bx ; DX:AX = AX * 10000 add ax,si ; AX = AX + mp_carry(SI) adc dx,0 ; Add carry flag stosw ; store LSW, [DI] = AX, DI=DI-2 mov si,dx ; save mp_carry(SI) for next iteration dec cx ; CX=CX-1 mov ax,[di] ; load into AX, and mult mul bx ; DX:AX = AX * 10000 add ax,si ; AX = AX + mp_carry(SI) adc dx,0 ; Add carry flag stosw ; store LSW, [DI] = AX, DI=DI-2 mov si,dx ; save mp_carry(SI) for next iteration loop -- ; CX=CX-1, JNZ ; end mult loop call prbcd ; print new MSW mov word ptr [di+2],0 ; zero MSW dec bp ; dec main loop counter jnz --- ; if not zero, more digits to print ; end main loop ret ; Description: Convert BIN/HEX to BCD. ; ; Input: AX = BIN/HEX (00h - 270Fh) ; ; Output: BCD (0000 - 9999) to screen ; ; Registers: AX, BX, DX prbcd: sub dx,dx ; clear DX mov bx,1000 ; BX = 1000 div bx ; AX / BX add al,'0' ; AL has MSB, convert to ASCII if quiet == 0 call cout endif mov ax,dx ; AX = DX, remainder mov bl,100 ; / 100 div bl add al,'0' ; AL has MSB, convert to ASCII if quiet == 0 call cout endif mov al,ah ; AL = AH, remainder sub ah,ah ; clear AH mov bl,10 ; / 10 div bl add al,'0' ; AL has MSB, convert to ASCII if quiet == 0 call cout endif mov al,ah ; print remainder add al,'0' ; AL has MSB, convert to ASCII if quiet == 0 call cout endif ret ; Description: Output a string. ; ; Input: BX address to array (a) ; ; Output: String to screen ; ; Registers: AL, BX print: mov al,[bx] ; load a character into AL and al,al ; update zero flag (faster than CMP AL,0) jz + ; if zero return call cout ; otherwise print it inc bx ; point to next character jmp print ; and jmp back for next char / ret ; Description: char/cr out via DOS INT 21H ; ; Input: AL = char ; ; Output: char to screen ; ; Registers: Nothing hosed crout: mov al,'\n' cout: push ax ; push AX if dos == 1 push dx ; push DX mov dl,al ; DL = AL mov ah,02h ; use DOS to print character int dosint pop dx ; restore DX else int coutint ; xt server output endif pop ax ; restore AX ret ;; Memory allocation for mp arrays: org $+($&1) ; force arrays on even boundary mp_a: org $+bin_len*2 ; x2 because of 16-bit words mp_b: org $+bin_len*2 mp_x: org $+bin_len*2 mp_y: org $+bin_len*2 ; ; *** Listing 2-5 *** ; ; The long-period Zen timer. (LZTIMER.ASM) ; Uses the 8253 timer and the BIOS time-of-day count to time the ; performance of code that takes less than an hour to execute. ; Because interrupts are left on (in order to allow the timer ; interrupt to be recognized), this is less accurate than the ; precision Zen timer, so it is best used only to time code that takes ; more than about 54 milliseconds to execute (code that the precision ; Zen timer reports overflow on). Resolution is limited by the ; occurrence of timer interrupts. ; ; By Michael Abrash 4/26/89 ; ; Externally callable routines: ; ; ZTimerOn: Saves the BIOS time of day count and starts the ; long-period Zen timer. ; ; ZTimerOff: Stops the long-period Zen timer and saves the timer ; count and the BIOS time-of-day count. ; ; ZTimerReport: Prints the time that passed between starting and ; stopping the timer. ; ; Note: If either more than an hour passes or midnight falls between ; calls to ZTimerOn and ZTimerOff, an error is reported. For ; timing code that takes more than a few minutes to execute, ; either the DOS TIME command in a batch file before and after ; execution of the code to time or the use of the DOS ; time-of-day function in place of the long-period Zen timer is ; more than adequate. ; ; Note: The PS/2 version is assembled by setting the symbol PS2 to 1. ; PS2 must be set to 1 on PS/2 computers because the PS/2's ; timers are not compatible with an undocumented timer-stopping ; feature of the 8253; the alternative timing approach that ; must be used on PS/2 computers leaves a short window ; during which the timer 0 count and the BIOS timer count may ; not be synchronized. You should also set the PS2 symbol to ; 1 if you're getting erratic or obviously incorrect results. ; ; Note: When PS2 is 0, the code relies on an undocumented 8253 ; feature to get more reliable readings. It is possible that ; the 8253 (or whatever chip is emulating the 8253) may be put ; into an undefined or incorrect state when this feature is ; used. ; ; *************************************************************** ; * If your computer displays any hint of erratic behavior * ; * after the long-period Zen timer is used, such as the floppy * ; * drive failing to operate properly, reboot the system, set * ; * PS2 to 1 and leave it that way! * ; *************************************************************** ; ; Note: Each block of code being timed should ideally be run several ; times, with at least two similar readings required to ; establish a true measurement, in order to eliminate any ; variability caused by interrupts. ; ; Note: Interrupts must not be disabled for more than 54 ms at a ; stretch during the timing interval. Because interrupts ; are enabled, keys, mice, and other devices that generate ; interrupts should not be used during the timing interval. ; ; Note: Any extra code running off the timer interrupt (such as ; some memory-resident utilities) will increase the time ; measured by the Zen timer. ; ; Note: These routines can introduce inaccuracies of up to a few ; tenths of a second into the system clock count for each ; code section timed. Consequently, it's a good idea to ; reboot at the conclusion of timing sessions. (The ; battery-backed clock, if any, is not affected by the Zen ; timer.) ; ; All registers and all flags are preserved by all routines. ; cpu 8086 ; ; Set PS2 to 0 to assemble for use on a fully 8253-compatible ; system; when PS2 is 0, the readings are more reliable if the ; computer supports the undocumented timer-stopping feature, ; but may be badly off if that feature is not supported. In ; fact, timer-stopping may interfere with your computer's ; overall operation by putting the 8253 into an undefined or ; incorrect state. Use with caution!!! ; ; Set PS2 to 1 to assemble for use on non-8253-compatible ; systems, including PS/2 computers; when PS2 is 1, readings ; may occasionally be off by 54 ms, but the code will work ; properly on all systems. ; ; A setting of 1 is safer and will work on more systems, ; while a setting of 0 produces more reliable results in systems ; which support the undocumented timer-stopping feature of the ; 8253. The choice is yours. ; PS2 equ 0 ; ; Base address of the 8253 timer chip. ; BASE_8253 equ 40h ; ; The address of the timer 0 count registers in the 8253. ; TIMER_0_8253 equ BASE_8253 + 0 ; ; The address of the mode register in the 8253. ; MODE_8253 equ BASE_8253 + 3 ; ; The address of the BIOS timer count variable in the BIOS ; data segment. ; TIMER_COUNT equ 46ch ; ; Macro to emulate a POPF instruction in order to fix the bug in some ; 80286 chips which allows interrupts to occur during a POPF even when ; interrupts remain disabled. ; MPOPF macro jmp $$p2 $$p1: iret ;jump to pushed address & pop flags $$p2: push cs ;construct far return address to call $$p1 ; the next instruction endm ; ; Macro to delay briefly to ensure that enough time has elapsed ; between successive I/O accesses so that the device being accessed ; can respond to both accesses even on a very fast PC. ; DELAY macro jmp $+2 jmp $+2 jmp $+2 endm StartBIOSCountLow dw ? ;BIOS count low word at the ; start of the timing period StartBIOSCountHigh dw ? ;BIOS count high word at the ; start of the timing period EndBIOSCountLow dw ? ;BIOS count low word at the ; end of the timing period EndBIOSCountHigh dw ? ;BIOS count high word at the ; end of the timing period EndTimedCount dw ? ;timer 0 count at the end of ; the timing period ReferenceCount dw ? ;number of counts required to ; execute timer overhead code ; ; String printed to report results. ; OutputStr: db 0dh, 0ah, "Timed count: " TimedCountStr: db 0,0,0,0,0,0,0,0,0,0 db " microseconds", 0dh, 0ah db 0 ; ; Temporary storage for timed count as it's divided down by powers ; of ten when converting from doubleword binary to ASCII. ; CurrentCountLow dw ? CurrentCountHigh dw ? ; ; Powers of ten table used to perform division by 10 when doing ; doubleword conversion from binary to ASCII. ; PowersOfTen: dd 1 dd 10 dd 100 dd 1000 dd 10000 dd 100000 dd 1000000 dd 10000000 dd 100000000 dd 1000000000 PowersOfTenEnd: ; ; String printed to report that the high word of the BIOS count ; changed while timing (an hour elapsed or midnight was crossed), ; and so the count is invalid and the test needs to be rerun. ; TurnOverStr: db 0dh, 0ah db "****************************************************" db 0dh, 0ah db "* Either midnight passed or an hour or more passed *" db 0dh, 0ah db "* while timing was in progress. If the former was *" db 0dh, 0ah db "* the case, please rerun the test; if the latter *" db 0dh, 0ah db "* was the case, the test code takes too long to *" db 0dh, 0ah db "* run to be timed by the long-period Zen timer. *" db 0dh, 0ah db "* Suggestions: use the DOS TIME command, the DOS *" db 0dh, 0ah db "* time function, or a watch. *" db 0dh, 0ah db "****************************************************" db 0dh, 0ah db 0 ;******************************************************************** ;* Routine called to start timing. * ;******************************************************************** ZTimerOn: ; ; Save the context of the program being timed. ; push ax pushf ; ; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause ; linear counting rather than count-by-two counting. Also stops ; timer 0 until the timer count is loaded, except on PS/2 ; computers. ; mov al,00110100b ;mode 2 out MODE_8253,al ; ; Set the timer count to 0, so we know we won't get another ; timer interrupt right away. ; Note: this introduces an inaccuracy of up to 54 ms in the system ; clock count each time it is executed. ; DELAY sub al,al out TIMER_0_8253,al ;lsb DELAY out TIMER_0_8253,al ;msb ; ; In case interrupts are disabled, enable interrupts briefly to allow ; the interrupt generated when switching from mode 3 to mode 2 to be ; recognized. Interrupts must be enabled for at least 210 ns to allow ; time for that interrupt to occur. Here, 10 jumps are used for the ; delay to ensure that the delay time will be more than long enough ; even on a very fast PC. ; pushf sti rept 10 jmp $+2 endm MPOPF ; ; Store the timing start BIOS count. ; (Since the timer count was just set to 0, the BIOS count will ; stay the same for the next 54 ms, so we don't need to disable ; interrupts in order to avoid getting a half-changed count.) ; push ds sub ax,ax mov ds,ax mov ax,ds:[TIMER_COUNT+2] mov cs:[StartBIOSCountHigh],ax mov ax,ds:[TIMER_COUNT] mov cs:[StartBIOSCountLow],ax pop ds ; ; Set the timer count to 0 again to start the timing interval. ; mov al,00110100b ;set up to load initial out MODE_8253,al ; timer count DELAY sub al,al out TIMER_0_8253,al ;load count lsb DELAY out TIMER_0_8253,al ;load count msb ; ; Restore the context of the program being timed and return to it. ; MPOPF pop ax ret ;******************************************************************** ;* Routine called to stop timing and get count. * ;******************************************************************** ZTimerOff: ; ; Save the context of the program being timed. ; pushf push ax push cx ; ; In case interrupts are disabled, enable interrupts briefly to allow ; any pending timer interrupt to be handled. Interrupts must be ; enabled for at least 210 ns to allow time for that interrupt to ; occur. Here, 10 jumps are used for the delay to ensure that the ; delay time will be more than long enough even on a very fast PC. ; sti rept 10 jmp $+2 endm ; ; Latch the timer count. ; if PS2 == 1 mov al,00000000b out MODE_8253,al ;latch timer 0 count ; ; This is where a one-instruction-long window exists on the PS/2. ; The timer count and the BIOS count can lose synchronization; ; since the timer keeps counting after it's latched, it can turn ; over right after it's latched and cause the BIOS count to turn ; over before interrupts are disabled, leaving us with the timer ; count from before the timer turned over coupled with the BIOS ; count from after the timer turned over. The result is a count ; that's 54 ms too long. ; else ; ; Set timer 0 to mode 2 (divide-by-N), waiting for a 2-byte count ; load, which stops timer 0 until the count is loaded. (Only works ; on fully 8253-compatible chips.) ; mov al,00110100b ;mode 2 out MODE_8253,al DELAY mov al,00000000b ;latch timer 0 count out MODE_8253,al endif cli ;stop the BIOS count ; ; Read the BIOS count. (Since interrupts are disabled, the BIOS ; count won't change.) ; push ds sub ax,ax mov ds,ax mov ax,ds:[TIMER_COUNT+2] mov cs:[EndBIOSCountHigh],ax mov ax,ds:[TIMER_COUNT] mov cs:[EndBIOSCountLow],ax pop ds ; ; Read the timer count and save it. ; in al,TIMER_0_8253 ;lsb DELAY mov ah,al in al,TIMER_0_8253 ;msb xchg ah,al neg ax ;convert from countdown ; remaining to elapsed ; count mov cs:[EndTimedCount],ax ; ; Restart timer 0, which is still waiting for an initial count ; to be loaded. ; if PS2 == 0 DELAY mov al,00110100b ;mode 2, waiting to load a ; 2-byte count out MODE_8253,al DELAY sub al,al out TIMER_0_8253,al ;lsb DELAY mov al,ah out TIMER_0_8253,al ;msb DELAY endif sti ;let the BIOS count continue ; ; Time a zero-length code fragment, to get a reference for how ; much overhead this routine has. Time it 16 times and average it, ; for accuracy, rounding the result. ; mov cs:[ReferenceCount],0 mov cx,16 cli ;interrupts off to allow a ; precise reference count RefLoop: call ReferenceZTimerOn call ReferenceZTimerOff loop RefLoop sti add cs:[ReferenceCount],8 ;total + (0.5 * 16) mov cl,4 shr cs:[ReferenceCount],cl ;(total) / 16 + 0.5 ; ; Restore the context of the program being timed and return to it. ; pop cx pop ax MPOPF ret ; ; Called by ZTimerOff to start the timer for overhead measurements. ; ReferenceZTimerOn: ; ; Save the context of the program being timed. ; push ax pushf ; ; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause ; linear counting rather than count-by-two counting. ; mov al,00110100b ;mode 2 out MODE_8253,al ; ; Set the timer count to 0. ; DELAY sub al,al out TIMER_0_8253,al ;lsb DELAY out TIMER_0_8253,al ;msb ; ; Restore the context of the program being timed and return to it. ; MPOPF pop ax ret ; ; Called by ZTimerOff to stop the timer and add the result to ; ReferenceCount for overhead measurements. Doesn't need to look ; at the BIOS count because timing a zero-length code fragment ; isn't going to take anywhere near 54 ms. ; ReferenceZTimerOff: ; ; Save the context of the program being timed. ; pushf push ax push cx ; ; Match the interrupt-window delay in ZTimerOff. ; sti rept 10 jmp $+2 endm mov al,00000000b out MODE_8253,al ;latch timer ; ; Read the count and save it. ; DELAY in al,TIMER_0_8253 ;lsb DELAY mov ah,al in al,TIMER_0_8253 ;msb xchg ah,al neg ax ;convert from countdown ; remaining to elapsed ; count add cs:[ReferenceCount],ax ; ; Restore the context and return. ; pop cx pop ax MPOPF ret ;******************************************************************** ;* Routine called to report timing results. * ;******************************************************************** ZTimerReport: pushf push ax push bx push cx push dx push si push di push ds ; push cs ;DOS functions require that DS point pop ds ; to text to be displayed on the screen assume ds:Code ; ; See if midnight or more than an hour passed during timing. If so, ; notify the user. ; mov ax,[StartBIOSCountHigh] cmp ax,[EndBIOSCountHigh] jz CalcBIOSTime ;hour count didn't change, ; so everything's fine inc ax cmp ax,[EndBIOSCountHigh] jnz TestTooLong ;midnight or two hour ; boundaries passed, so the ; results are no good mov ax,[EndBIOSCountLow] cmp ax,[StartBIOSCountLow] jb CalcBIOSTime ;a single hour boundary ; passed-that's OK, so long as ; the total time wasn't more ; than an hour ; ; Over an hour elapsed or midnight passed during timing, which ; renders the results invalid. Notify the user. This misses the ; case where a multiple of 24 hours has passed, but we'll rely ; on the perspicacity of the user to detect that case. ; TestTooLong: ;mov ah,9 ;mov dx, TurnOverStr ;int dosint mov bx,TurnOverStr call print jmp ZTimerReportDone ; ; Convert the BIOS time to microseconds. ; CalcBIOSTime: mov ax,[EndBIOSCountLow] sub ax,[StartBIOSCountLow] mov dx,54925 ;number of microseconds each ; BIOS count represents mul dx mov bx,ax ;set aside BIOS count in mov cx,dx ; microseconds ; ; Convert timer count to microseconds. ; mov ax,[EndTimedCount] mov si,8381 mul si mov si,10000 div si ;* .8381 = * 8381 / 10000 ; ; Add timer and BIOS counts together to get an overall time in ; microseconds. ; add bx,ax adc cx,0 ; ; Subtract the timer overhead and save the result. ; mov ax,[ReferenceCount] mov si,8381 ;convert the reference count mul si ; to microseconds mov si,10000 div si ;* .8381 = * 8381 / 10000 sub bx,ax sbb cx,0 mov [CurrentCountLow],bx mov [CurrentCountHigh],cx ; ; Convert the result to an ASCII string by trial subtractions of ; powers of 10. ; mov di, PowersOfTenEnd - PowersOfTen - 4 mov si, TimedCountStr CTSNextDigit: mov bl,'0' CTSLoop: mov ax,[CurrentCountLow] mov dx,[CurrentCountHigh] sub ax,PowersOfTen[di] sbb dx,PowersOfTen[di+2] jc CTSNextPowerDown inc bl mov [CurrentCountLow],ax mov [CurrentCountHigh],dx jmp CTSLoop CTSNextPowerDown: mov [si],bl inc si sub di,4 jns CTSNextDigit ; ; ; Print the results. ; ;mov ah,9 ;mov dx, OutputStr ;int dosint mov bx,OutputStr call print ; ZTimerReportDone: pop ds pop di pop si pop dx pop cx pop bx pop ax MPOPF ret