SIMPLY FPU
by Raymond Filiatreault

Chap. 13
Commented example

The example presented in this chapter is based on a simple dialog box to compute the monthly payments on a mortgage based on the following formula:

where:
P = Mortgage Principle
R = Monthly Interest Rate (expressed in decimal)
N = Number of months
PMT = Computed monthly payments

The dialog box itself was designed with the "WYSIWYG" feature of the Symantec Resource Editor Rs32.exe provided with the MASM32 package in the rc sub-folder. The only manual modification to the generated script was to add the ES_NUMBER style to two of the edit controls, that style not being an option with Rs32. The resource script is reproduced at the end of this document.

Three separate edit controls are provided for the user to input the mortgage principal, the annual interest rate (expressed as a %) and the number of years. Two radio buttons are also provided to choose between two procedures for computing monthly payments.

One procedure is based on the generally allowed practice in the USA of compounding interest on a monthly basis. For example, a 12% stated annual rate becomes a 1% monthly rate which, when compounded, is equivalent to a true 12.6825% annual rate.

The other procedure is based on a restriction imposed on Canadian chartered banks for mortgage loans where the stated annual rate cannot be compounded more than twice per year. For example, a 12% stated annual rate becomes a 6% semi-annual rate which, when compounded, is equivalent to a true 12.36% annual rate. The monthly rate must thus be computed according to the following formula where RSA is the semi-annual rate:

R = (1+RSA)1/6 - 1

When this computed monthly rate is compounded on a monthly basis, it would be equivalent to the semi-annual rate.

In order to simplify the program as much as possible, some limits have been imposed on the user input but may not even be noticed.

-- The input for the mortgage principal is restricted to numbers only (with the ES_NUMBER style for its edit control) and limited to a maximum of 9 characters (with the EM_SETLIMITTEXT message during the WM_INITDIALOG phase). This still allows for a mortgage input of up to 1 billion dollars which should generally be sufficient, but disallows the inclusion of pennies (which would be very rarely specified anyway) as part of the input. The main advantage of those restrictions is that it guarantees a positive binary 32-bit integer can be retrieved directly from the edit control without any need to parse the input for invalid characters and perform the conversion from ASCII with additional code.

-- The input for the number of years is also restricted to numbers only and limited to a maximum of 2 characters. This still allows for a mortgage life of up to 99 years, but disallows partial years (which is also rarely specified) as part of the input. Advantages are the same as above.

-- The input for the annual rate is restricted to 9 characters (rates are rarely specified with more than 3 decimals). This, however, guarantees that the value of the numerical digits excluding the decimal delimiter would not exceed the maximum possible value of a positive 32-bit integer.

While parsing the annual rate, a few more restrictions generate an error message:

-- An annual rate exceeding 100% (which would be considered illegal loan-sharking). This guarantees that the log2 of any (1+R) term will always be less than 1.

-- A "-" sign. The rate must be positive.

A lack of input or an input equal to 0 in any of the three edit controls also generates an error message and no computation is performed.

With pre-validated data, the computation can thus proceed without any risk of error. Some code has nevertheless been added before displaying the result to ascertain that no major problem has been encountered due to unforeseeable circumstances.

The code has been kept as simple as possible, without any attempt to optimize it for speed or size (speed optimization would be a waste of time and effort for such an application). It is also specific for the application. Even the code to convert the annual rate from a string to a floating point should not be generalized without modifying the parts which rely on the designed purpose and restrictions.

The provided code is fully tested. It can be assembled without modification with MASM32 if copied into a file with the .asm extension. If the resource script is copied into a file named rsrc.rc and placed in the same directory as the .asm file, the .exe file can be generated in a single step with the Project->Build All menu option of the QEditor in MASM32. Modifications to the code and/or assembly procedures will be required with other assemblers and/or IDEs.

The FPU instructions used with this example application are (in alphabetical order):

```F2XM1     2 to the X power minus 1
FBSTP     Store BCD data to memory
FCHS      Change the sign of ST(0)
FDIV      Divide two floating point values
FIDIV     Divide ST(0) by an Integer located in memory
FIMUL     Multiply ST(0) by an Integer located in memory
FINIT     Initialize the FPU
FLD1      Load the value of 1
FMUL      Multiply two floating point values
FRNDINT   Round ST(0) to an integer
FSCALE    Scale ST(0) by ST(1)
FSTP      Store real number and pop ST(0)
FSTSW     Store status word
FSUB      Subtract two floating point values
FWAIT     Wait while FPU is busy
FXCH      Exchange the top data register with another data register
FYL2XP1   Y*Log2(X+1)```

```; #######################################################################
;
;                    Mortgage payment calculator
;            Written with MASM32 by Raymond Filiatreault
;                           August 2003
;
; #######################################################################

.386                   ; minimum processor needed for 32 bit
.model flat, stdcall   ; FLAT memory model & STDCALL calling
option casemap :none   ; set code to case sensitive

; #######################################################################

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\comctl32.inc

includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\comctl32.lib

; #########################################################################

return MACRO arg
mov eax, arg
ret
ENDM

WndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

; #########################################################################

PRINCIPLE   EQU   711
RATEPCT     EQU   712
YEARS       EQU   713
PAYMENT     EQU   714
COMPUTE     EQU   720
QUITS       EQU   721
USABUT      EQU   730
AMERICAN    EQU   0

.data
hDlg        dd    0
hInstance   dd    0

mortgage    dd    0
months      dd    0

factor6     dd    6
factor10    dd    10
factor12    dd    12

bcdtemp     dt    0

princerr    db    "Unacceptable input for principle",0
raterr      db    "Unacceptable input for rate",0
yearerr     db    "Unacceptable input for years",0
invalid     db    "Invalid FPU operation detected",0

buffer1     db    16 dup(0)

; #########################################################################

.code

start:

invoke GetModuleHandle,NULL
mov    hInstance,eax
invoke InitCommonControls

invoke ExitProcess,eax

; #########################################################################

WndProc proc hWin   :DWORD,
uMsg   :DWORD,
wParam :DWORD,
lParam :DWORD

.if uMsg == WM_INITDIALOG
push  hWin
pop   hDlg        ;save handle in a global variable
;this avoids having to pass it as a
;parameter whenever needed outside this proc

invoke SendDlgItemMessage,hDlg,PRINCIPLE,EM_SETLIMITTEXT,9,0
invoke SendDlgItemMessage,hDlg,RATEPCT,EM_SETLIMITTEXT,9,0
invoke SendDlgItemMessage,hDlg,YEARS,EM_SETLIMITTEXT,2,0
return TRUE

.elseif uMsg == WM_COMMAND
mov     eax,wParam
and     eax,0ffffh

.if   eax == QUITS            ;Exit button clicked
invoke  EndDialog,hDlg,0

.elseif eax == COMPUTE        ;Compute button clicked
call  maincalc          ;process input and display result
return  TRUE

.elseif eax == USABUT         ;USA radiobutton clicked
return  TRUE

return  TRUE
.endif

.elseif uMsg == WM_CLOSE
invoke  EndDialog,hDlg,0

.endif

return FALSE        ;use Windows defaults to handle other messages

WndProc endp

; ########################################################################

maincalc:

;*********************************************************
;*****     Retrieve THE MORTGAGE PRINCIPAL input     *****
;*********************************************************

invoke GetDlgItemInt,hDlg,PRINCIPLE,0,0       ;retrieve as an integer
mov   mortgage,eax      ;store it
or    eax,eax           ;check if input > 0
ja    @F                ;jump if OK
lea   eax,princerr      ;error message for input of principal

inputerror:
ret

;******************************************************
;*****     Retrieve the NUMBER OF YEARS input     *****
;******************************************************

@@:
invoke GetDlgItemInt,hDlg,YEARS,0,0           ;retrieve as an integer
mul   factor12          ;convert it to months
mov   months,eax        ;store it
or    eax,eax           ;check if input > 0
ja    @F                ;jump if OK
lea   eax,yearerr       ;error message for input of years
jmp   inputerror

;**************************************************
;*****     Retrieve the ANNUAL RATE input     *****
;**************************************************

@@:
invoke GetDlgItemText,hDlg,RATEPCT,ADDR buffer1,16 ;retrieve as string
.if   buffer1 == 0      ;buffer1 being a global BYTE variable
;if there was no input in the EDIT control,
;the first byte of buffer1 would be 0

lea   eax,raterr    ;error message for annual rate input
jmp   inputerror
.endif

call  atofl             ;convert ASCII string (%) to REAL10 decimal
;->st(0)=decimal annual rate (if no error)

; Note: The "atofl" sub-routine is called only once
; and its code could have been inserted directly
; without the need for the "call" instruction. This
; was done for the purpose of clarity in this section.

or    eax,eax           ;check if error was detected
jz    badrate           ;abort and display message if error

;*************************************************************
;*****     Convert the annual rate to a MONTHLY rate     *****
;*************************************************************

fidiv factor12      ;divide the annual rate by 12
;-> st(0)=monthly rate

.else
fld1          ;-> st(0)=1, st(1)=annual rate
fchs          ;-> st(0)=-1, st(1)=annual rate
fxch          ;-> st(0)=annual rate, st(1)=-1 scaling factor
fscale        ;divide the annual rate by 2^1
;to get the semi-annual rate
;(a positive scaling factor will multiply
;a negative scaling factor will divide)
fstp  st(1)   ;overwrite the scaling factor with st(0)
;and pop the top register
;-> st(0)=semi-annual rate
fld1          ;-> st(0)=1, st(1)=semi-annual rate
fidiv factor6 ;-> st(0)=1/6, st(1)=semi-annual rate
fxch          ;-> st(0)=semi-annual rate, st(1)=1/6
fyl2xp1       ;-> st(0)=[log2(semi-annual rate+1)]*1/6

;Note: Because of limitations imposed on input for size and sign,
;the (semi-annual rate+1) term will be between 1 and 1.5
;and its log2 will be positive and less than 1. That log2
;is further divided by 6 for a value definitely between
;0 and +1. That value can thus be used directly with
;the next instruction without any need for scaling before
;or after.

f2xm1         ;-> st(0)=monthly rate (obtained directly
;because the instruction provides the minus 1)
;When this monthly rate is compounded 6 times
; it would be equal to the semi-annual rate
.endif

;************************************************
;*****     Compute the monthly payments     *****
;************************************************

fild  months      ;-> st(0)=months, st(1)=monthly rate (R)
fld   st(1)       ;-> st(0)=R, st(1)=months, st(2)=R
fyl2xp1           ;-> st(0)=log2(1+R)*months, st(1)=R
fld   st          ;-> st(0)=log, st(1)=log, st(2)=R
frndint           ;-> st(0)=int(log), st(1)=log, st(2)=R
fsub  st(1),st    ;-> st(0)=int(log), st(1)=log-int(log), st(2)=R
fxch  st(1)       ;-> st(0)=log-int(log), st(1)=int(log), st(2)=R
f2xm1             ;-> st(0)=2[log-int(log)]-1, st(1)=int(log), st(2)=R
fld1
fscale            ;-> st(0)=(1+R)N, st(1)=int(log), st(2)=R
fstp  st(1)       ;-> st(0)=(1+R)N, st(1)=R
fld   st          ;-> st(0)=(1+R)N, st(1)=(1+R)N, st(2)=R
fld1              ;-> st(0)=1, st(1)=(1+R)N, st(2)=(1+R)N, st(3)=R
fsub              ;-> st(0)=(1+R)N-1, st(1)=(1+R)N, st(2)=R
fdiv              ;-> st(0)=(1+R)N/[(1+R)N-1], st(1)=R
fmul              ;-> st(0)=R*(1+R)N/[(1+R)N-1]
fimul mortgage    ;-> st(0)=P*R*(1+R)N/[(1+R)N-1]=Monthly payments
fimul factor10    ;multiply by 100 to have 2 decimal places as integer
fimul factor10    ;-> st(0)=Monthly payments*100
fbstp bcdtemp     ;store in memory in BCD format
;rounded to the closest penny

;*************************************************************************
; The folowing section of code is not necessary for this application
; because every precaution was taken to examine the input to insure that
; the data used in the FPU computations is valid and would not result in
; any major error. It is merely included to indicate how to check the
; validity of the end result whenever there may be a risk of invalid data.
; It also insures that the FBSTP instruction is completed before starting
; to access the stored packed BCD data.
;*************************************************************************

fstsw ax          ;copy to AX the FPU's Status Word
;containing the exception flags
fwait             ;insure the execution is completed
shr   eax,1       ;transfer bit0 to the CPU's carry flag
;That bit would be set if an invalid operation
;was detected with any of the FPU instructions
;The final result would then be invalid
jnc   @F          ;continue if no invalid operation flag

lea   eax,invalid

;****************************************************************
;*****         Unpack the BCD result and display it         *****
;
; The coding used is not suitable as a general purpose unpacking
; algorithm for at least two reasons: the sign is known to be
; positive and thus disregarded, and the result was designed to
; always contain 2 decimal places.
;***************************************************************

@@:
push  esi               ;preserve ESI and EDI
push  edi
lea   esi,bcdtemp+8     ;use ESI to point initially
;to the 2nd most significant byte
lea   edi,buffer1       ;use EDI to point to the buffer
;where the ASCII string will be stored
mov   ecx,8             ;use ECX as counter for the number of bytes
;possibly containing integer digits

;Note: (The BCD format has 10 bytes. The most significant
;byte contains the sign which is disregarded in this
;application, and the least significant byte is known
;to contain two decimal digits.)

;**********************************************************************
; Search for the most significant byte containing the 1st integer digit
;**********************************************************************

@@:
movzx eax,byte ptr[esi] ;get next byte in AL zero extended in EAX
dec   esi         ;adjust pointer to next byte
or    eax,eax     ;check if it contains the 1st integer
jnz   @F          ;jump if found
dec   ecx         ;decrement counter
jnz   @B          ;continue search until all integer bytes checked

;***********************************************
; If no integer digit is present,
; place a "0" digit before the decimal delimiter
;***********************************************

mov   al,"0"
stosb             ;insert the "0" digit
jmp   decimals    ;go insert the decimal delimiter and digits

;*******************************************************
; If that 1st byte contains only 1 integer digit,
; it has to be processed separately to avoid a leading 0
;*******************************************************

@@:
test  al,0f0h     ;check the high nibble of AL
jnz   @F          ;jump if there are 2 integer digits
add   al,"0"      ;convert the digit to ASCII
stosb             ;insert that digit
jmp   nextdigit   ;process next byte

;*******************************************************************
; Other bytes contain 2 integer digits each which must be unpacked
; The high nibble digit must be followed in memory by the low nibble
;*******************************************************************

@@:
ror   ax,4              ;transfer the high nibble to low nibble of AL
;and low nibble of AL to high nibble of AH
ror   ah,4              ;transfer it to the low nibble of AH
add   ax,3030h          ;convert both to ASCII
stosw                   ;insert both, AL followed by AH in memory

nextdigit:
movzx eax,byte ptr[esi] ;get next byte
dec   ecx               ;decrement integer byte counter
jnz   @B                ;continue with integer digits until completed

decimals:
mov   byte ptr[edi],"." ;insert the decimal delimiter
ror   ax,4              ;unpack the decimal byte as above
ror   ah,4
stosw
mov   byte ptr[edi],0   ;insert the terminating 0

pop   edi               ;restore the EDI and ESI registers
pop   esi

;***********************************************************************
; Display the result in the dialog box and return control to the WndProc
;***********************************************************************

ret

;***************************************************************
;                            atofl
;
; This sub-routine is partly general purpose and partly specific
; for the task. It parses the string for unacceptable characters
; but also returns an error if the integral portion of the input
; exceeds 100. It also returns an error for a negative sign.
;
; The conversion approach of treating all the numerical digits as
; being integer digits is sound only because the size of the
; string was limited to 9 characters in the EDIT control. This
; guarantees that, in a worse case scenario, the maximum integer
; value of the input would still fit in a 32-bit register. The
; final integer value obtained is then corrected according to the
; count of decimal digits in the input string.
;
; Returns with EAX = 0 if error detected.
;
; If EAX != 0, the converted annual rate is returned in st(0)
;****************************************************************

atofl:
push  ebx         ;preserve EBX and ESI
push  esi

lea   esi,buffer1 ;use ESI as pointer to text buffer
xor   eax,eax
xor   ebx,ebx     ;will be used as an accumulator
xor   ecx,ecx     ;will be used as a counter

;************************************************
; Skip leading spaces without generating an error
;************************************************

@@:
lodsb             ;get next character
cmp   al," "      ;check if a space character
jz    @B          ;repeat until a non-space character is found

;*********************************************
; Check 1st non-space character for a +/- sign
;*********************************************

cmp   al,"-"      ;is it a "-" sign
jnz   @F

atoflerr:
xor   eax,eax     ;set EAX to error code
pop   esi         ;restore the EBX and ESI registers
pop   ebx
ret               ;return with error code

@@:
cmp   al,"+"      ;is it a "+" sign
jnz   nextchar
lodsb             ;disregard a "+" sign and get next character

;***********************************************************
; From this point, space and sign characters will be invalid
;***********************************************************

nextchar:
cmp   al,0        ;check for end-of-string character
jz    endinput    ;exit the string parsing section

cmp   al,"."      ;is it the "." decimal delimiter
;other delimiters such as the "," used in some
;countries could also be allowed but would need
;additional coding to make it more generalized
jnz   @F

;******************************************************************
; Only one decimal delimiter can be acceptable. The sign bit of ECX
; is used to keep a record of the first delimiter identified.
;******************************************************************

or    ecx,ecx     ;check if a delimiter has already been identified
js    atoflerr    ;exit with error code if more than 1 delimiter

stc               ;set the carry flag
rcr   ecx,1       ;set bit31 of ECX (the sign bit) when
;the 1st delimiter is identified
lodsb             ;get next character
jmp   nextchar    ;continue parsing

;***********************************************************************
; All ASCII characters other than the numerical ones will now be invalid
;***********************************************************************

@@:
cmp   al,"0"
jb    atoflerr
cmp   al,"9"
ja    atoflerr

sub   al,"0"      ;convert valid ASCII numerical character to binary
xchg  eax,ebx     ;get the accumulated integer value in EAX
;holding the new digit in EBX
mul   factor10    ;multiply the accumulated value by 10
xchg  eax,ebx     ;store this new accumulated value back in EBX

or    ecx,ecx     ;check if a decimal delimiter detected yet
js    @F          ;jump if decimal digits are being processed

;*************************************
; Integer digits still being processed
;*************************************

cmp   ebx,100     ;verify current value of integer portion
ja    atoflerr    ;abort if input for annual rate is > 100%
lodsb             ;get next string character
jmp   nextchar    ;continue processing string characters

;*******************************************************
; The CL register is used as a counter of decimal digits
; after the decimal delimiter has been identified
;*******************************************************

@@:
inc   cl          ;increment count of decimal digits
lodsb             ;get next string character
jmp   nextchar    ;continue processing string characters

;***********************************
; Parsing of the string is completed
;***********************************

endinput:
or    ebx,ebx     ;check if total input was equal to 0
jz    atoflerr    ;abort if annual rate input is 0%

finit             ;initialize FPU
push  ebx         ;store value of EBX on stack
fild  dword ptr[esp]    ;-> st(0)=EBX
add   cl,2        ;increment the number of decimal digits
;to convert from % rate to a decimal rate
shl   ecx,1       ;get rid of the potential sign "flag"
shr   ecx,1       ;restore the count of decimal digits
fild  factor10    ;-> st(0)=10, st(1)=EBX
@@:
fdiv  st(1),st    ;-> st(0)=10, st(1)=EBX/10
dec   ecx         ;decrement counter of decimal digits
jnz   @B          ;continue dividing by 10 until count exhausted
fstp  st          ;get rid of the dividing 10 in st(0)
;-> st(0)=annual rate (as a decimal rate)
pop   ebx         ;clean CPU stack

pop   esi         ;restore the EBX and ESI registers
pop   ebx
or    al,1        ;insure EAX != 0 (i.e. no error detected)
ret

;*******************************

end start```

```;#######################################################################
;
;                    Resource script for the Dialog Box
;                 to be used with the Mortgage Calculator
;                               August 2003
;
;#######################################################################

#include "\masm32\include\resource.h"

STYLE DS_MODALFRAME | 0x0004 | WS_CAPTION | WS_SYSMENU | WS_VISIBLE | WS_POPUP
CAPTION "Mortgage Payments"
FONT 8, "MS Sans Serif", 700, 0 /*FALSE*/
BEGIN
LTEXT     "Mortgage principle", 701, 4,6,66,10, SS_LEFT, , 0
LTEXT     "Annual rate, %", 702, 	4,22,66,10, SS_LEFT, , 0
LTEXT     "Number of years", 703, 	4,38,66,10, SS_LEFT, , 0
LTEXT     "Monthly payments", 704, 	4,84,66,10, SS_LEFT, , 0
EDITTEXT  711,  78, 6,49,11, ES_RIGHT | ES_NUMBER, , 0
EDITTEXT  712,  78,22,49,11, ES_RIGHT, , 0
EDITTEXT  713, 102,38,25,11, ES_RIGHT | ES_NUMBER, , 0
CONTROL   "", 714, "Edit", ES_READONLY | ES_RIGHT, 	78,84,49,11, , 0
CONTROL   "Compute", 720, "Button", BS_DEFPUSHBUTTON, 16,111,41,13, , 0
CONTROL   "Exit", 721, "Button", 0, 	74,111,41,13, , 0
CONTROL   "U.S.A.", 730, "Button", BS_AUTORADIOBUTTON | WS_GROUP, 18,62,44,10, ,0