Lupine Systems Presents...Part Two of the Series
Build Your Own Microcontroller Calculator
THIS MONTH:   Build a Floating-Point LCD/VFD Calculator
by Spike Tsasmali, Lupine Systems

Software for the Floating-Point Calculator

In this section I will provide a detailed explination of how the software performs each of the math functions included in this calculator.

Much of the code used for the floating-point calculator is the same as the code used for the fixed-point version, so I will only present code examples that are uniquely used in the floating-point version.

Samples of code will be presented for each of the four basic math functions, but due to length, not all of the utility subroutines will be listed in this article. To see the full unabridged, uncensored, unedited version of the code, open the file, lcdcalc.asm (included in the Project Package using any standard text editor, or by creating and opening a project using MPLAB IDE. Also refer to last month's article, Software for the Basic Fixed-Point Calculator for examples of the basic code structures re-used by the floating-point routines.

Several macros are also used within the code to simplify basic PIC functions. These macros are:

  • BANK0 -- Bank Select 0, sets to Bank 0, Clears the RP0 bit , same as BCF STATUS,5
  • BANK1 -- Bank Select 1, sets to Bank 1, Sets the RP0 bit, same as BSF STATUS,5
  • PAGE0 -- ROM Page Select 0, sets to Page 0, Clears the PCLATH,3 bit, same as BCF PCLATH,3
  • PAGE1 -- ROM Page Select 1, sets to Page 1, Sets the PCLATH,3 bit, same as BSF PCLATH,3
  • So in the following code examples, the use of the two macros PAGE0 and PAGE1 refer to ROM page addressing and can be omitted if the routines and their calls are located in the lower page of memory.

    Format

    A functionally operational mathematical calculator can be realized using several different techniques. This project incorporates the technique of performing math operations using BCD (Binary Coded Decimal) unpacked numbers. This means that for each number entered on the keyboard there is a separate memory location for that number. Typically when working with BCD numbers it is common practice to "pack" the digits, or combine two of the 4-bit BCD numbers into a single 8-bit word where the most significant nibble is the "tens" place digit and the least significant nibble is the "ones" place. This saves memory space but can complicate matters later on. Instead, I have chosen to handle the numbers individually, with ZERO as the most significant digit of the byte. This technique requires more RAM memory but has its advantages when it comes to smooth and quick math operations.

    Setting Up the Registers

    Unlike the fixed-point calculator, the floating-point calculator needs more and larger registers. Some of the same registers are used as were on the fixed-point version and some are used for the same purpose, only with more cells.

    The calculator requires two main operational registers, each 8 digits long, representing the (X) register and the (Y) register. There also needs to be an "overflow" byte one step higher than the most significant byte (MSB) of the actual 8-digit number and a "sign" byte representing the numerical polarity (positive or negative). Therefore, the (X) and the (Y) register file stack is 10 bytes wide each.

    There also needs to be several internal registers used by the operation routines as "scratchpad" registers. These registers vary in size. In this project, the addition and subtraction routines have been extended beyond 8 digits to accomodate the decimal alignment. Therefore, addition and subtraction use the (X) and the (Y) registers and re-use the (P) and the (T) registers. Since floating-point multiplication differs from fixed-point only in the placement of the decimal, the multiply routine uses the same registers as the fixed-point--two very lengthy registers, the (T) register, or totalizer register, and the (P) register, or the product register. During multiplication, the software will actually calculate the product to 16 digits and then reduce the product to 8 digits. This lenthy process is necessary in the event an operation such as 88888888 * 1 is done. The division routine however is a total wash from the original software in almost every way, although the original divide subroutine is actually where the division takes place. Fixed point division requires several special registers, the (B) register or the "bank" register and the (Q) register or the quotient register. There is also a (Z) register used to store the dividend data during the division process. For floating-point, a much lengthier final quotient register (V) has been added so that decimal shifting can be done. A separate modulus register has also been added to carry the modulus data from division call to division call (J).

    The floating-point calculator also offers several math functions such as 1/x, x2 and MEAN. Since these operations affect ONLY the (X) register AND re-use the four basic functions to perform each task, the contents of the (Y) register must be saved in a special, isolated register (R). Memory also requires a separate and isolated register bank (M) along with sign and decimal registers.

    All together, 140 RAM locations are dedicated to the register files.

    Register Assignments

    (B) is 8 bytes wide plus decimal register, used by the divdr subroutine
    (J) is 8 bytes wide, used by the floating division routine
    (M) is 10 bytes wide plus decimal register, and is the calculator's memory
    (P) is 17 bytes wide, used by the multiplication, SUB16 and ADD16 routines
    (Q) is 8 bytes wide, used by the divdr subroutine
    (R) is 10 bytes wide plus decimal register, and is used as an isolated save for (Y)
    (T) is 20 bytes wide, used by the multiplication, SUB16 and ADD16 routines
    (V) is 25 bytes wide, used by the floating division routine
    (X) is 10 bytes wide plus decimal register, the machine's main accumulator
    (Y) is 10 bytes wide plus decimal register, the machine's secondary operand register
    (Z) is 8 bytes wide plus decimal register, used by the divdr subroutine
    Registers MEANCTR,MEAN1,MEAN2,MEAN3 are used for the MEAN calculation divisor

    As you can see most of the registers are used either by the divdr subroutine or the division routine. This is simply because division is the most complicated of the four basic functions.

    Since each operation uses sequential file banks, each of these registers' individual files (or cells) MUST be sequential and cannot be re-arranged to compress the code.

    There also need to be several "flag" bits used as steering control bits. Therefore there are three RAM locations reserved for these flag bits.

    Doing the Math

    There are only two basic functions included in the calculator -- addition and subtraction. These two functions are further broken down into Fixed-point and Floating-Point routines. Multiplication and division are derivatives of both addition and subtraction. Since any operation on two variables can be quantumized into a set of "rules" that apply to the signage, all operations can be unsigned and deal only with positive integers. Once the operation is complete, the sign is then dealt with. This simplifies the entire sequence of events and makes it so the main addition and main subtraction routines can be "re-used" by the multiplication and division routines without modification.

    The calculator will use the ADD16 routine to add two numbers where there is a decimal in either of the two numbers, and the SUB16 for subtraction. If there are no decimals, the older ADD8 and SUB8 routines are used to make the machine a bit faster. Multiplication is exactly the same for floating-point as it is for fixed-point except for the placement of the decimal so an additional call to a "discriminator" routine is necessary to not only position the decimal properly but to determine how many decimal places can be displayed after the integer component within the display size (8 digits). Division requires a sequencer to call division "cycles" but the actual division is the exact same thing as used in the fixed-point machine.

    In order to deal with the sign, a set of mathematical rules must apply to all equations. These rules are derivatives of the Axioms of Addition, Multiplication and Division.

    The Rules

    Addition:

  • If (X) is positive and (Y) is positive, then the math operation is ADD and the result is positive.
  • If (X) is positive and (Y) is negative, then the operation is SUBTRACT and the result is TRUE (whatever the sign comes out to be).
  • If (X) is negative and (Y) is positive, then the operation is SUBTRACT and the result is TRUE.
  • If (X) is negative and (Y) is negative, then the operation is ADD and the result is negative.
  • Subtraction:

  • If (X) is positive and (Y) is positive, then the math operation is SUBTRACT and the result is TRUE.
  • If (X) is positive and (Y) is negative, then the operation is SUBTRACT and the result is negative.
  • If (X) is negative and (Y) is positive, then the operation is ADD and the result is positive.
  • If (X) is negative and (Y) is negative, then the operation is ADD and the result is TRUE.
  • Multiplication:

  • If (X) AND (Y) are both positive, OR (X) and (Y) are both negative, then multiply, the result is positive.
  • If either (X) or (Y) is negative, then multiply, the result is negative.
  • Division:

  • If (X) AND (Y) are both positive, OR (X) and (Y) are both negative, then divide, the result is positive.
  • If either (X) or (Y) is negative, then divide, the result is negative.
  • Unpacked BCD Unsigned Floating-Point Addition

    Unpacked BCD Addition adds the number in the (X) register to the number in the (Y) register.

    This is easily done by mimicking the way you would add up two numbers on paper.

    But before you can add two decimal point numbers on paper you must first line up the decimals. This is done by scrutinizing the digit data saved in (X) by shifting the data to fit within a "range" of area inside a much larger register.

    Here's How:

    Example: X = 12.345, Y = 5678.9

    First copy the two variables into larger registers. The (T) and the (P) registers used by the 8-digit multiply routine works fine.

    Remember that the contents of each register does not indicate the location of the decimal point. The decimal point location is saved in two separate registers, DPx and DPy.

    Next, designate a location within each of the registers where the break point occurs between the fraction and the integer. The lower part of the (P) register would be a good place, so all of the digits from (P1) to (P8) are the fraction part of the number and all the digits from (P9) to (P16) are the integer part. These location designations will be the same for the (T) register with (T9) and up being the integer and (T8) and below the fraction. This "aligns" the decimals in both numbers to the same cell assignments in both registers.

    In a calculator, all of the leading zeroes are valid numbers, so in our example, 12.345 would actually be 00012.345 and 5678.9 would be 0005678.9 .

    P8P7P6P5P4P3P2P1
    0005678.9
    00012.345

    Notice that the decimals do not line up in a single column. This has to be corrected before addition can be done. The (P) register must be "shifted" so that the "9" is in cell (P8) and the "8" is in cell (P9). The (T) register also must be shifted so that the "3" is in cell (T8) and the "2" is in cell (T9).

    The register DPy tells us that the decimal point is at the 1st decimal location (the far right display location is the "0" decimal location). So when copying (Y) to (P) this can tell the software where to begin copying the integer component. So first, locate the first integer and subtract it from the decimal location, then subtract that result from 8 (8 digits on the display). This will be the first location in (P) to begin copying (Y). When done, the integer component will all be above (P9) and the fraction part will all be below (P8), inclusive.

    Do the same for (X) and copy it to the (T) register. Then add as if it were a regular fixed-point add except 16-digits long (Refer to the section on Addition in last month's article Software for the Basic Fixed-Point Calculator for more information on how the actual addition routine runs).

    To finalize the effort, the display must be "discriminated", or "clipped" down to 8-digits. It would be fit and proper to bias the display in favor of the integer component. So a discriminator is run which once again measures the integer part to see how long the integer is and if there is any room left over for the fraction component. Then to make the display more presentable, any trailing zeroes are removed from the fraction component.

    Here is the PIC code for how this is done:

    !!WARNING!! Long code example including several service subroutines!

    
    ;****************************************************************************
    ;16-DIGIT FLOATING POINT ADDER 
    ;****************************************************************************
    
    ADD16	CLRF	XSIGN		;CLEAR X AND Y SIGN REGS
    	CLRF	YSIGN
    
    	CLRF	P17		;BE SURE CARRY/STATUS REG IS CLEAR!
    
    	MOVLW	P1		;SET UP FSRP, FSRT POINTERS
    	MOVWF	FSRP
    	MOVLW	T1
    	MOVWF	FSRT
    
    	MOVLW	016
    	MOVWF	ADDCNTR		;LOOP COUNTER
    
    SUM16	MOVF	FSRP,0
    	MOVWF	FSR
    
    	MOVF	INDR,0		;GET T DIGIT
    	MOVWF	TEMP2		;SAVE
    
    	MOVF	FSRT,0		;SETUP T DIGIT
    	MOVWF	FSR
    	
    	MOVF	TEMP2,0		;RETRIEVE P DIGIT FROM TEMP	
    	ADDWF	INDR,0		;ADD TO T
    	PAGE0
    	CALL	ADDBCD		;CONVERT
    	PAGE1
    	MOVWF	TEMP		;SAVE
    	MOVF	FSRP,0
    	MOVWF	FSR
    	MOVF	TEMP,0		;RETRIEVE
    
    	MOVWF	INDR		;SAVE ON P
    	SWAPF	INDR,0		;SWAP TO FIND CARRY IF ANY
    	ANDLW	B'00001111'	;MASK OUT HIGH NIBBLE
    	INCF	FSR,1		;POINT TO NEXT BYTE IN P
    	ADDWF	INDR,1		;ADD!
    	DECF	FSR,1		;RESTORE P POINTER
    	MOVLW	B'00001111'	;CORRECT CARRY BITS IN P REG
    	ANDWF	INDR,1		;SAVE FINAL P VALUE
    	
    	INCF	FSRP,1		;INCREMENT DATA POINTERS
    	INCF	FSRT,1
    	DECFSZ	ADDCNTR,1	;LOOP 16 DIGITS
    	GOTO	SUM16
    
    	CALL	DISCRIM		;DISCRIMINATE DIGITS
    
    	CLRF	XSIGN		;ALWAYS POSITIVE
    	MOVF	P17,0		;OVERFLOW?
    	BTFSC	Z
    	RETURN			;NO OVERFLOW
    
    	PAGE0
    	CALL	XCLEAR		;CLEAR OFF GARBAGE...
    	PAGE1
    	MOVLW	0x01
    	MOVWF	X9		;ERROR CODE FOR OVERFLOW
    	RETURN
    
    ;****************************************************************************
    ;8-DIGIT ADD/SUBTRACT DISCRIMINATOR SUBROUTINE, INTEGER DOMINANT
    ;****************************************************************************
    
    DISCRIM	PAGE0
    	CALL	XCLEAR		;CLEAR THE (X) REGISTER
    	PAGE1
    
    	MOVLW	P16
    	MOVWF	FSR		;LOAD FSR WITH HIGHEST INT
    
    	MOVLW	08
    	MOVWF	DIGCTR		;LOOP UP TO 8 TIMES
    
    FINDINT	MOVF	INDR,0		;GET (P) VALUE
    
    	BTFSS	Z
    	GOTO	INT1		;FIRST INTEGER FOUND
    
    	DECF	FSR,1		;POINT TO NEXT (P) CELL
    	DECFSZ	DIGCTR,1
    	GOTO	FINDINT	
    
    INT1	MOVF	DIGCTR,0
    	BTFSC	Z		;IF ZERO INTEGERS, ALL FRACTIONS
    	GOTO	NOADINTS	;NO INTEGERS!
    
    	MOVF	DIGCTR,0
    	MOVWF	TEMP2		;SAVE THIS FOR FRACTION
    
    	MOVLW	P8
    	ADDWF	DIGCTR,0	;FIGURE OUT FIRST INT LOCATION
    
    	MOVWF	FSRP		;PUT IN (P) POINTER
    
    	MOVLW	X8		;LOAD UP (X)
    	MOVWF	FSRX
    
    MVINT	MOVF	FSRP,0
    	MOVWF	FSR		;POINT TO (P)
    
    	MOVF	INDR,0		;READ IN (P)
    	MOVWF	TEMP		;SAVE IN TEMP
    
    	MOVF	FSRX,0
    	MOVWF	FSR
    	
    	MOVF	TEMP,0
    	MOVWF	INDR		;SAVE (P) TO (X)
    
    	DECF	FSRX,1
    	DECF	FSRP,1		;NEXT BYTES
    
    	DECFSZ	DIGCTR,1	;NEXT!
    	GOTO	MVINT
    
    	MOVF	TEMP2,0		;FIGURE OUT DECIMALS
    	SUBLW	08
    	
    	MOVWF	DPX		;FOUND DECIMAL POSITION
    	
    	BTFSC	Z
    	RETURN			;NO DECIMALS!
    
    ZAINT	MOVLW	P8		;FIRST DEC POSITION
    	MOVWF	FSRP		;SET UP (P) POINTER
    
    	MOVLW	X1
    	ADDWF	DPX,0		;FIGURE OUT (X) POS
    	MOVWF	FSRX
    	DECF	FSRX,1		;OFFSET!
    
    	MOVF	DPX,0
    	MOVWF	DIGCTR		;LOOP COUNTER
    
    MVFRAC	MOVF	FSRP,0
    	MOVWF	FSR		;SET UP (P) POINTER
    
    	MOVF	INDR,0
    	MOVWF	TEMP		;SAVE IN TEMP
    
    	MOVF	FSRX,0
    	MOVWF	FSR		;SET UP (X) POINTER
    
    	MOVF	TEMP,0
    	MOVWF	INDR
    
    	DECF	FSRX,1
    	DECF	FSRP,1		;NEXT BYTES
    
    	DECFSZ	DIGCTR,1
    	GOTO	MVFRAC
    
    	CALL	ZEROTRAIL	;RID TRAILING ZEROS
    
    	RETURN
    
    NOADINTS
    	MOVLW	08		;ALL FRACTIONS
    	MOVWF	DPX
    	GOTO	ZAINT		;RE-ENTER FRACTION ROUTINE
    
    ;****************************************************************************
    ;16-DIGIT DECIMAL SPLITTER SUBROUTINE
    ;****************************************************************************
    
    SPLIT16	PAGE0
    	CALL	PCLEAR
    	CALL	TCLEAR
    	PAGE1
    	MOVLW	T9		;FIRST DIGIT OF INTEGER
    	MOVWF	TEMP
    
    	MOVF	DPX,0		;GET DEC POSITION
    	SUBWF	TEMP,0		;MAKE SUBTRACTION
    	MOVWF	FSRT		;PUT IN POINTER REGISTER
    
    	MOVLW	X1
    	MOVWF	FSRX		;SET UP (X) POINTER
    
    	MOVLW	08
    	MOVWF	DIGCTR		;LOOP COUNTER
    
    XTLP	MOVF	FSRX,0		;GET (X) POINTER DATA
    	MOVWF	FSR		;PUT IN FSR
    	
    	MOVF	INDR,0		;READ IN (X) DATA
    	MOVWF	TEMP2		;SAVE IN TEMP2
    
    	MOVF	FSRT,0
    	MOVWF	FSR		;SET UP (T) POINTER
    
    	MOVF	TEMP2,0
    	MOVWF	INDR		;MOVE TO (T) REGISTER
    
    	INCF	FSRX,1
    	INCF	FSRT,1		;NEXT POINTERS
    
    	DECFSZ	DIGCTR,1	;LOOP
    	GOTO	XTLP		;COPY LOOP
    
      ;CENTER (Y) WITHIN (P) REGISTER
    
    	MOVLW	P9		;FIRST DIGIT OF INTEGER
    	MOVWF	TEMP
    
    	MOVF	DPY,0		;GET DEC POSITION
    	SUBWF	TEMP,0		;MAKE SUBTRACTION
    	MOVWF	FSRP		;PUT IN POINTER REGISTER
    
    	MOVLW	Y1
    	MOVWF	FSRY		;SET UP (Y) POINTER
    
    	MOVLW	08
    	MOVWF	DIGCTR		;LOOP COUNTER
    
    YPLP	MOVF	FSRY,0		;GET (Y) POINTER DATA
    	MOVWF	FSR		;PUT IN FSR
    	
    	MOVF	INDR,0		;READ IN (Y) DATA
    	MOVWF	TEMP2		;SAVE IN TEMP2
    
    	MOVF	FSRP,0
    	MOVWF	FSR		;SET UP (P) POINTER
    
    	MOVF	TEMP2,0
    	MOVWF	INDR		;MOVE TO (P) REGISTER
    
    	INCF	FSRY,1
    	INCF	FSRP,1		;NEXT POINTERS
    
    	DECFSZ	DIGCTR,1	;LOOP
    	GOTO	YPLP		;COPY LOOP
    
    	RETURN
    
    ;*************************************************************
    ;TRAILING ZEROS SUPRESSION SUBROUTINE
    ;*************************************************************
    		
    ZEROTRAIL
    	MOVLW	X1
    	MOVWF	FSR		;SET UP POINTER TO (X)
    
    	MOVF	INDR,0		;READ IN (X) CELL
    
    	BTFSS	Z		;TEST FOR ZERO
    
    	RETURN			;DONE!
    
    	MOVF	X2,0
    	MOVWF	X1
    
    	MOVF	X3,0
    	MOVWF	X2
    
    	MOVF	X4,0
    	MOVWF	X3
    	
    	MOVF	X5,0
    	MOVWF	X4
    
    	MOVF	X6,0
    	MOVWF	X5
    	
    	MOVF	X7,0
    	MOVWF	X6
    	
    	MOVF	X8,0
    	MOVWF	X7
    
    	MOVLW	00
    	MOVWF	X8
    	
    	DECFSZ	DPX,1		;ADJUST POINT
    	GOTO	ZEROTRAIL	;NO MORE ZEROS!
    
    	RETURN
    
    
    Unpacked BCD Unsigned Floating-Point Subtraction

    Most of the following explination is exactly the same as that for the fixed-point subtractor described in last month's article. The only difference between fixed and floating point is the alignment of the decimal in larger registers--done exactly the same way using the same subroutines as used in the floating-point addition described earlier.

    Unpacked BCD Subtraction subtracts the number in the (X) register from the number in the (Y) register.

    It is done in exactly the same manner as Unpacked BCD Addition is done, except for when the difference between Y and X forms a negative number. In this case, the subtraction has to be done twice in order to reverse the negativity of the difference, or in other words, convert the 2's compliment into a positive integer.

    Here's How:

    Example: X = 56789, Y = 12345

        12345 (Y register value)
      -56789 (X register value)
      ---------
      -44444 (difference)

    Notice that the difference above (-44444) is the same as if you "got rid of the signs, reversed the two terms and subtracted, then made the sign negative". This would work but ONLY if (Y) were always positive. So if we simply went ahead and subtracted the larger number from the smaller number, the software can essentially "borrow on credit" a "1" at the top of the (Y) register stack, leaving the (Y9) register in "underflow" state, and the (Y1) through (Y8) registers representing a 2's compliment result.

    Therefore to solve this problem, unlike addition, the rules MUST be considered before the subtraction routine is called. If (X) is numerically greater than (Y) then the subtraction subroutine must be called TWICE -- the first time to subtract the greater number from the lesser, and the second time to subtract the 2's compliment from zero and "borrow on credit" the necessary "1" in the (Y) MSB to complete the subtraction.

    Once the second subtraction is completed, then the RULES will assign the sign to the difference.

    Remember, if (X) is numerically less than (Y) the subtraction subroutine only has to be run ONCE.

    To finalize floating point subtraction, the final answer is discriminated and the trailing zeros are removed.

    Here's the PIC code for how this is done:

    
    SUB16	CLRF	X9		;CLEAR OVERFLOW REGISTERS
    	CLRF	Y9		
    	CLRF	XSIGN
    	CLRF	YSIGN		;CLEAR SIGN REGISTERS TOO!
    
    	CALL	SUBT1D		;DO UNSIGNED SUBTRACTION
    
    	MOVF	P17,0		;IF P17 IS ZERO THEN WE'RE DONE!
    
    	BTFSC	Z
    	GOTO	DSDIM		;ANSWER POSITIVE, IN (T) REGISTER		
    
      ;IF WE'RE HERE THEN NUMBER IS AUTOMATICALLY NEGATIVE!
    
    	PAGE0
    	CALL	PCLEAR		;CLEAR (P) REGISTER
    	PAGE1
    	CALL	SUBT1D		;SUBTRACT FROM ZERO
    
    	CALL	PTSWAP		;SWAP (T) AND (P)
    	CALL	DISCRIM		;DISCRIMINATE DIGITS
    
    	MOVF	P17,0
    	BTFSC	Z
    	GOTO	SUBUNDR16	;UNDERFLOW
    
    	CLRF	XSIGN		;NO UNDERFLOW
    	BSF	XSIGN,0		;SET SIGN BIT NEGATIVE
    	CLRF	X9		;CLEAR ERROR REGISTER
    	RETURN
    	
    SUBUNDR16
    	CLRF	XSIGN
    	MOVLW	0x01		;SHOW UNDERFLOW ERROR
    	MOVWF	X9
    	RETURN	
    
    DSDIM	CALL	PTSWAP		;SWAP (T) AND (P)
    	CALL	DISCRIM		;DISCRIMINATE DIGITS
    	CLRF	X9
    	CLRF	XSIGN		;NO UNDERFLOW, POSITIVE
    
    	RETURN
    	
    ;*************************************************************
    ;16-DIGIT SUBTRACTOR--SUBTRACTS T FROM P UNSIGNED
    ;*************************************************************
    
    SUBT1D	BANK1
    	CLRF	OVER16		;CLEAR OVERFLOW REG
    	BANK0
    	MOVLW	T1		;SET UP FSRP FSRT POINTERS
    	MOVWF	FSRT
    	MOVLW	P1
    	MOVWF	FSRP
    
    	MOVLW	016
    	MOVWF	ADDCNTR		;LOOP COUNTER
    
    NXSUB16	MOVF	FSRT,0
    	MOVWF	FSR		;SET UP T POINTER
    
    	MOVF	INDR,0
    	MOVWF	TEMP2		;SAVE T VALUE
    
    	MOVF	FSRP,0
    	MOVWF	FSR		;SET UP P POINTER
    
    	MOVF	TEMP2,0
    	SUBWF	INDR,0		;SUBTRACT!
    	PAGE0
    	CALL	SUBBCD		;CONVERT
    	PAGE1
    
    	MOVWF	TEMP2		;SAVE CONVERTED DATA
    
    	MOVF	FSRT,0
    	MOVWF	FSR		;SET UP T POINTER
    
    	MOVF	TEMP2,0
    	MOVWF	INDR		;SAVE ON T REGISTER
    
    	SWAPF	INDR,0
    	ANDLW	B'00001111'
    	MOVWF	TEMP2		;SAVE MASKED DATA
    	
    	MOVF	FSRP,0
    	MOVWF	FSR		;SET UP P POINTER
    	INCF	FSR,1
    
    	MOVF	TEMP2,0
    	SUBWF	INDR,1		;UPDATE P REGISTER
    
    	MOVF	FSRT,0
    	MOVWF	FSR		;SET UP T POINTER
    		
    	MOVLW	B'00001111'
    	ANDWF	INDR,1		;CLEAR OFF CARRY BITS
    
    	INCF	FSRT,1		;UPDATE FSR POINTERS
    	INCF	FSRP,1
    	DECFSZ	ADDCNTR,1
    	GOTO	NXSUB16		;LOOP
    
    	RETURN
    
    

    Unpacked BCD Unsigned Floating-Point Multiplication

    Since floating-point multiplication is exactly the same as fixed-point multiplication except for the simple placement of the decimal, the actual multiply software is exactly the same as that for the fixed-point routine used in last month's article. The decimal is handled by a simple decimal handling subroutine called after the main multiplier sub call.

    To place the decimal, the rule of math says to take the number of decimal places in (X) and the number of decimal places in (Y) and add them together, then shift the decimal to the LEFT this sum of places. This is done to the (P) register before the number is sent on to the (X) register and display.

    First (DPx) and (DPy) are added together to find out how many shifts to do. Then a loop counter is established with this value and the shift to the left takes place until the loop is zero.

    Next and finally, the (P) register is discriminated to be integer-dominant and to format the product to fit the 8-digit display.

    Since the actual multiply is the same as for fixed-point, only the decimal adjust routine will be presented here.

    Here's the PIC code for how this is done:

    
    DECADJ	MOVWF	DPY		;FROM EARLIER ADD, DEC POS
    
    	PAGE0
    	CALL	XCLEAR		;CLEAR THE (X) REGISTER
    	PAGE1
    
      ;DETERMINE IF (P) NEEDS TO BE SHIFTED OR NOT
    
    	MOVLW	08
    	XORWF	DPY,0
    	BTFSC	Z
    	GOTO	MXNOSHIFT	;NO SHIFT REQUIRED
    
      ;DETERMINE SHIFT DIRECTION
    
    	MOVF	DPY,0
    	SUBLW	08
    
    	BTFSS	CARRY		
    	GOTO	MSHR
    
      ;SHIFT LEFT OPERATION
    
    	MOVWF	DIGCTR		;SAVE FROM SUBTRACTION EARLIER
    	
    MSL2	MOVLW	16
    	MOVWF	M1CTR		;LOOP COUNTER
    
    	MOVLW	P17		;SET UP POINTERS
    	MOVWF	FSRP1
    
    	MOVLW	P16
    	MOVWF	FSRP
    
    LSLP	MOVF	FSRP,0
    	MOVWF	FSR		;(P) POINTER
    
    	MOVF	INDR,0		;READ (P)
    	MOVWF	TEMP		;SAVE IN TEMP
    
    	MOVF	FSRP1,0
    	MOVWF	FSR		;(P+1) POINTER
    
    	MOVF	TEMP,0
    	MOVWF	INDR		;MOVE DATA!
    	
    	DECF	FSRP,1
    	DECF	FSRP1,1		;DECREMENT POINTERS
    
    	DECFSZ	M1CTR,1
    	GOTO	LSLP
    	
    	CLRF	P1		;SET P1 TO ZERO EACH TIME!
    
    	DECFSZ	DIGCTR,1
    	GOTO	MSL2
    	
    	GOTO	MXNOSHIFT	;DETECT FOR OVERFLOW NOW!
    
    MSHR	MOVLW	08
    	SUBWF	DPY,0		;DETERMINE SHIFT POSITION
    
    	MOVWF	DIGCTR		;SAVE FROM SUBTRACTION EARLIER
    	
    MSR2	MOVLW	16
    	MOVWF	M1CTR		;LOOP COUNTER
    
    	MOVLW	P1		;SET UP POINTERS
    	MOVWF	FSRP1
    
    	MOVLW	P2
    	MOVWF	FSRP
    
    LSRP	MOVF	FSRP,0
    	MOVWF	FSR		;(P+1) POINTER
    
    	MOVF	INDR,0		;READ (P)
    	MOVWF	TEMP		;SAVE IN TEMP
    
    	MOVF	FSRP1,0
    	MOVWF	FSR		;(P) POINTER
    
    	MOVF	TEMP,0
    	MOVWF	INDR		;MOVE DATA!
    	
    	INCF	FSRP,1
    	INCF	FSRP1,1		;INCREMENT POINTERS
    
    	DECFSZ	M1CTR,1
    	GOTO	LSRP
    	
    	CLRF	P17		;SET P17 TO ZERO EACH TIME!
    
    	DECFSZ	DIGCTR,1
    	GOTO	MSR2
    	
    MXNOSHIFT
    	MOVF	P17,0
    	BTFSC	Z
    	GOTO	DODISC		;DISCRIMINATE
    
    	PAGE0
    	CALL	XCLEAR		;CLEAR OFF TRASH...
    	
    	MOVLW	0x01		;UNDERFLOW ERROR CODE
    	MOVWF	X9
    	
    	RETURN
    
    DODISC	CALL	DISCRIM		;DISCRIMINATE
    	RETURN
    

    Unpacked BCD Unsigned Floating-Point Division

    Floating-point division is accomplished by repeatedly calling the 8-digit fixed-point division routine over and over until a certain resolution of accuracy is achieved.

    The first time the divdr subroutine is called the integer component of the quotient is solved. There is no more action necessary to complete the integer divide.

    However, it may take an unknown number of loops to fully divide out to any particular given accuracy the fraction component. The more calls, the more digits, thus the more accurate.

    The first two divdr calls have certain requirements. Each and every call afterwards will fall into a separate category.

    These differences are due to the way the modulus component (undivisable remainder) is left over after each division stage has completed. In the first call, the integer component is solved. Whatever is left over is called the modulus and is passed on to the next division stage. The next stage will be shifted to the RIGHT by 1 position. If there are no integers in the quotient, there still remains a "0" (zero) to the left of the decimal and of course, zero as a whole number is always part of a fraction. This "leading" zero must first be shifted away before the first of the fractional divisions can be associated with the integer component.

    Since there will be many divisions done, there is the need for an extraordinarily long register to put all of these digits together at the end. So a new register, the (V) register is established, and is 25 cells in size.

    First, (Y) is divided by (X) as usual. The result is then moved into the upper 1/3 of the (V) register (V17) through (V24). The modulus component is passed on to the next stage of division to be used as the dividend. The modulus is then shifted all the way to the LEFT of the (Y) register before the division call is made. This sets up the quotient component to be the next set of numbers in-line to the previously divided integer component. Although shifted, the quotient, which will be the first "8" digits of the fraction, is off by 1 digit to the RIGHT due to the leading zero. So to correct for this alignment and to properly place the decimal, the quotient is then shifted to the LEFT by the number of digits in the modulus. This will properly align the first digit of the quotient with the location of the decimal in the (V) register. Then the quotient is copied into the center set of cells within the (V) register (V9) through (V15).

    To extend the accuracy of the division, one more additional stage of division is done. The modulus component left from the first fractional division is transferred to the (Y) register and once again, shifted to the far LEFT. This time, unlike earlier division cycles, the quotient from this stage will be shifted to the LEFT by the sum of the modulus digits from the first fractional division (Stage 2)AND this division (Stage 3). This process will continue for each additional stage of division added to the software. Each time a division is called, the number of modulus digits is added to a "running total" of modulus digits and accumulated so that further stages can be added later (The running total does not need to include any values from the Stage 1 division since there is no modulus in Stage 1, esentially it would be zero). So if you wanted to add another stage, you would once again take the modulus from the last stage, move it to the (Y) register, then shift the (Y) register to the far LEFT, call divdr, then shift the quotient to the LEFT the sum of all of the modulus digits, starting with the very first (integer) division. This number of shifts can be quite a few after several stages but it is necessary to align the digits to the position within the (V) register.

    After the third division, the quotient is then moved into the lowest 8 digits of the (V) register (V1) through (V8). Since the (Q) register is only 8-digits long, the LEFT shift is actually done by copying the highest cell address in the (Q) register to the proper address location within the (V) register. So you must first determine which cell in the (V) register will accept the highest cell in the (Q) register. This is done by subtracting the number of accumulated modulus digits from the fixed decimal location (P16). Therefore, there is actually no physical "shifting"; it is all done by math in software.

    Additional stages of division can be added as long as the difference [(P16) - (sum of modulus digits)] is greater than or equal to 1, and there are locations within the (V) register to copy the stage results to. Remember that each division stage results in an 8-digit quotient, then this quotient is shifted to the LEFT by the running total of modulus digits. Therefore, if the stage quotient returns only one or two useable digits, you may be able to add up to 6 more stages.

    The actual "divide" is the exact same routine as used by the fixed-point division routine so only the cycle handler routine is shown here.

    Here's the PIC code for how this is done:

    
    DIVIDE	MOVLW	08
    	MOVWF	DIGCTR		;DIVIDE BY ZERO CHECK
    	
    	CLRF	TEMP		;USE TEMP AS CHECKSUM REG
    	
    	MOVLW	X1
    	MOVWF	FSR		;DO X REGISTER CHECKSUM
    
    XZCHECK	MOVF	INDR,0
    	ADDWF	TEMP,1		;ADD IT UP!
    	INCF	FSR,1		;NEXT BYTE
    	
    	DECFSZ	DIGCTR,1
    	GOTO	XZCHECK		;LOOP AROUND...
    
    	MOVF	TEMP,0
    	BTFSC	Z		;IF ZERO, THEN X=0, NO GO!
    	GOTO	DE2		;EXIT IN ERROR
    
    	MOVF	DPX,0
    	BANK1
    	MOVWF	DPX1
    	BANK0
    	MOVF	DPY,0
    	BANK1
    	MOVWF	DPY1		;SAVE DP REGS
    	BANK0
    	PAGE0
    	CALL	VCLEAR
    	PAGE1
    
    	BTFSC	XSIGN,0		;CHECK FOR POSITIVE X
    	GOTO	DIVNEG		;X IS POSITIVE
    
    	BTFSC	YSIGN,0		;CHECK FOR POSITIVE Y
    	GOTO	DIVNEG2		;Y IS POSITIVE
    	GOTO	DIVPOS
    
    DIVNEG	BTFSC	YSIGN,0		;IF BOTH NEG, THEN POS RESULT
    	GOTO	DIVPOS		;DO POS!
    
    DIVNEG2	BANK1
    	BSF	DIVNEGFLAG	;DIVIDE NEGATIVELY!
    	BANK0			;NOW DIVIDE!
    
      ;STAGE 1 DIVIDE
    
    DIVPOS	CALL	XJCOPY		;SAVE (X) FOR FRACTION
    	PAGE0	
    	CALL	DIVDR		;DO DIVIDE BOTH POSITIVE
    	PAGE1
    
    	CALL	XVHCOPY		;COPY INTO (V-HI)
    
    	MOVLW	08
    	BANK1
    	MOVWF	MODUL		;MODULUS COUNTER
    TSYSH1	BANK0
    	MOVF	Y8,0		;SHIFT!
    	BTFSS	Z
    	GOTO	NSH1
    	CALL	YSHIFT		;SHIFT Y POSITION
    	BANK1
    	DECFSZ	MODUL,1		;UPDATE MODULUS COUNTER
    	GOTO	TSYSH1
    	BANK0
    
      ;STAGE 2 DIVIDE
    
    NSH1	BANK1
    	MOVF	MODUL,0
    	MOVWF	MSAV		;SAVE FIRST MODULUS
    	BANK0
    	CALL	JXCOPY		;LOAD (X)
    	PAGE0
    	CALL	DIVDR
    	PAGE1
    	
    DVXSHL1	BANK0
    	CALL	XSHIFT		;SHIFT (X) LEFT
    
    	BANK1
    	DECFSZ	MODUL,1
    	GOTO	DVXSHL1		;LOOP!
    
    	BANK0
    	
    	CALL	XVMCOPY		;COPY TO (V-MID)
    
    	MOVLW	08
    	BANK1
    	MOVWF	MODUL		;MODULUS COUNTER
    TSYSH2	BANK0
    	MOVF	Y8,0		;SHIFT!
    	BTFSS	Z
    	GOTO	NSH2
    	CALL	YSHIFT		;SHIFT Y POSITION
    	BANK1
    	DECFSZ	MODUL,1		;UPDATE MODULUS COUNTER
    	GOTO	TSYSH2
    	BANK0
    
      ;STAGE 3 DIVIDE
    
    NSH2	CALL	JXCOPY		;LOAD (X)
    	PAGE0
    	CALL	DIVDR
    	PAGE1
    	
    DVXSHL2	BANK0
    	CALL	XSHIFT		;SHIFT (X) LEFT
    
    	BANK1
    	DECFSZ	MODUL,1
    	GOTO	DVXSHL2		;LOOP!
    
    	BANK0
    
    	BANK1
    	MOVLW	V8
    	ADDWF	MSAV,0		;CALCULATE COPY POS
    	BANK0
    	MOVWF	TEMP2
    
    	CALL	XVLCOPY		;COPY TO (V-LO)
    
    	BANK1
    	MOVF	DPX1,0		;GET (X) DECIMAL REG
    	BANK0
    	MOVWF	DIGCTR		;LOOP COUNTER
    
    	BTFSC	Z
    	GOTO	DSHR1		;NO SHIFT LEFT
    
    VLDS1	CALL	VLSHIFT		;LEFT SHIFT
    
    	DECFSZ	DIGCTR,1	;LOOP
    	GOTO	VLDS1
    
    DSHR1	BANK1
    	MOVF	DPY1,0		;GET (Y) DECIMAL REG
    	BANK0
    	MOVWF	DIGCTR		;LOOP COUNTER
    
    	BTFSC	Z
    	GOTO	NDIVSH		;NO SHIFTING
    
    VRDS1	CALL	VRSHIFT		;SHIFT RIGHT
    	DECFSZ	DIGCTR,1
    	GOTO	VRDS1
    
    NDIVSH	CALL	VDISCRIM	;DISCRIMINATE!
    
    	CLRF	XSIGN		;SET POSITIVE
    	
    	BANK1
    	BTFSC	DIVNEGFLAG
    	GOTO	NEGDIV
    	BANK0
    	GOTO	DIVERROR
    NEGDIV	BCF	DIVNEGFLAG
    	BANK0
    	BSF	XSIGN,0		;SET NEGATIVE
    
    DIVERROR
    	BANK1			;TEST FOR ERROR CONDITION
    	MOVF	V25,0
    	BANK0
    	BTFSC	Z
    	RETURN			;DONE!
    	
    DE2	PAGE0
    	CALL	XCLEAR		;CLEAR X AND DISPLAY
    	PAGE1
    	MOVLW	01
    	MOVWF	X9		;SET X ERROR REGISTER
    	RETURN			;RETURN TO MAIN 
    

    Advanced Math Functions

    Now that the calculator can do floating-point arithmetic, more advanced math functions can be added.

    Some of the more obvious features are to add a reciprocal key, percent key and MEAN, or average key.

    Reciprocal simply divides the number 1 by the number on the display. A simple handling routine moves the data in the (Y) register to a safekeeping register (R), loads (Y) with 1 and calls the division routine. It finalizes up by restoring the original (Y) data.

    Percent is also easily implemented. Once again, the (Y) register is saved on (R). Then 2 is added to register (DPx) to shift the decimal two places to the left (percent). Then the multiply routine is called. Once again, to finalize the function, the (Y) register is restored from (R).

    MEAN is done by counting the number of times the M+ key is pressed in a separate register MEANCTR. Then, once the MEAN key is pressed, the MEANCTR register is broken up from a binary number into a 3-digit unpacked decimal number and moved into the (X) register. Then the data in memory (M) is moved to the (Y) register. Finally the divide routine is called and the (Y) register is restored.

    Each of these functions are implemented as semi-functions within the key strobe routine. This is most practical since the solutions to these functions only affect the (X) register and can be passed on to pending operations or become the first entry in a new operation.

    A Final Note

    The routines outlined in this section can and could be modified for processor effiency, however these routines were written for speed at the expense of processor resources.

    Let me suggest that you play around with the code, add additional functions or port the code to a more potent PIC such as a 17C42 or an 18LF4552. With a more powerful processor you could realize one mean machine!

    Which is exactly what we did! Click Here to go to the next article, "Build an Advanced Floating-Point Nixie Tube Desk Calculator".

    Back to the Lupine Systems Homepage
    Lupine Systems Download Section
    How to Make PC Boards
    How to Use EasyTrax CAD Software

    © 2007 Lupine Systems. It's a WOLF Thing! ®