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:

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, x^{2} 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:**

**Subtraction:**

**Multiplication:**

**Division:**

*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 .

P8 | P7 | P6 | P5 | P4 | P3 | P2 | P1 | ||

0 | 0 | 0 | 5 | 6 | 7 | 8 | . | 9 | |

0 | 0 | 0 | 1 | 2 | . | 3 | 4 | 5 |

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

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! ®