SIMPLY FPU
by Raymond Filiatreault
Copyright 2003

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
FADD      Add two floating point values
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
FILD      Load integer from memory
FIMUL     Multiply ST(0) by an Integer located in memory
FINIT     Initialize the FPU
FLD       Load real number
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
CANADABUT   EQU   731
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

      radiobutton db    0           ;0=USA, 1=Canada

      badinput    db    "Input error",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 DialogBoxParam,hInstance,700,NULL,ADDR WndProc,NULL

      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
            invoke CheckRadioButton,hDlg,USABUT,CANADABUT,USABUT
            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
                  mov   radiobutton,0     ;store info
                  return  TRUE
                  
            .elseif eax == CANADABUT      ;Canada radiobutton clicked
                  mov   radiobutton,1     ;store info
                  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:
      invoke  MessageBox,0,eax,ADDR badinput,MB_OK  ;display error message
      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

badrate:
          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     *****
;*************************************************************

      .if   radiobutton == AMERICAN
          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
      fadd              ;-> st(0)=2[log-int(log)], st(1)=int(log), st(2)=R
      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 following 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
      jmp   inputerror  ;display error message and return to WndProc

;****************************************************************
;*****         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   esi               ;adjust pointer
      dec   ecx               ;decrement integer byte counter
      jnz   @B                ;continue with integer digits until completed

decimals:
      mov   byte ptr[edi],"." ;insert the decimal delimiter
      inc   edi               ;adjust string pointer
      ror   ax,4              ;unpack the decimal byte as above
      ror   ah,4
      add   ax,3030h
      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
;***********************************************************************

      invoke SetDlgItemText,hDlg,PAYMENT,ADDR buffer1
      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)
; already divided by 100.
;****************************************************************

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
      add   eax,ebx     ; and add the new digit
      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"

700 DIALOGEX MOVEABLE IMPURE LOADONCALL DISCARDABLE	100, 80, 131, 132, 0
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
    CONTROL   "Canada", 731, "Button", BS_AUTORADIOBUTTON, 75,62,44,10, , 0
END



RETURN TO
SIMPLY FPU
CONTENTS