﻿ Conversion of floating point numbers to decimal in AVR assembler Path: Home => AVR-EN => assembler introduction => Floating point numbers => Conversion to decimal    (Diese Seite in Deutsch: ) Beginner's introduction to AVR assembler language

Converting floating point numbers to decimal in assembler language

To convert floating point binaries into decimal (ASCII) numbers we need, of course, some binary and exponential math. If you are weak in both disciplines and if you are lazy, do not try to understand the following, pick a floating point library instead. If you really want to know how it works: go on reading, it is not too complicated to understand.

Allocation of numbers

As has been shown on the previous page, a 24-bit binary consists of 16 bits for the mantissa and of 8 bits for the exponent. Both components take their most significant bit as sign. We can easily store these two components in three bytes, e. g. in three registers of the AVR.

The decimal resolution of such a binary number is 4 1/2 digits. To convert these back to decimal we need some more space as each digit needs one byte. So we better place the decimal result, together with some interim numbers that are needed during conversion to the SRAM, so we do not need to mess around with register needs and shortages. You can also increase the resolution simply by extending the SRAM reservation, and the software adds more steps.

15 bits mantissa in binary format corresponds with five decimal digits (215 = 32,768). The format of the result is as follows: We need the mantissa's sign bit (if positive we use a blank), the normalized first digit, then the decimal dot, four significant and one insignificant digits, then the E, the exponent's sign (+ or -) and two exponent digits. In total we are at 12 bytes result.

So this is the space that should be reserved for the result. To reserve space for that in assembler, we write:

.dseg
.org SRAM_START
DecAsc:
.byte 12 ; Reserve 12 bytes for result

The conversion involves adding decimal numbers with
• a resolution of six digits, so we can handle more precision than needed,
• an additional space for three further digits, with which we can do the rounding at the end,
• to the left we add another digit to allow for overflows when multiplying the decimal by two during conversion of the binary exponent.
So we are at 10-digit numbers for handling the decimal digits. We will need two buffers for that: one for calculation of the mantissa's value and one to prepare the adders of the mantissa's bits. We add some space to place those numbers on the beginning of a line in SRAM to ease reading in simulation, but can leave these reservations aside when space gets scarce. Our SRAM space now looks like this:

.dseg
.org SRAM_START
.equ MantLength = 10
sMant:
.byte MantLength
sMantEnd:
.byte 16-MantLength
.byte MantLength
.byte 16-MantLength
sDecAsc:
.byte 12

The two End: labels are for checking if the end has been reached, or, in case we have to start from the end of the number, to place a pointer right behind the number.

A basic decision is to handle the calculations in simple binary format, where 0 to 9 are handled as binaries 0 to 9. This requires one byte per digit and does not involve the H flag (in case of packed BCD) or the ASCII format bits when handling ASCII numbers. This is much simpler than in other formats, but needs slightly more time.

In the first step we init the stackpointer, because we use subroutines.

The second step is to get rid of the mantissa's sign bit. If bit 7 of the mantissa is zero we can skip the following. If it is one we subract the LSB from zero and invert the MSB. This makes a positive number from the negative. The decimal mantissa, and also the adder space, with its eight bytes each now look like this.

Converting the mantissa to decimal Conversion starts with bit 14 of the binary mantissa. As this bit is always a one, we can skip this by setting the result as well as the adder to 0.50000000. We would formulate this in assembler as follows:

; Initiate the decimal mantissa
InitMant:
ldi ZH,High(sMant) ; Point Z to mantissa space, MSB
ldi ZL,Low(sMant) ; dto., LSB
clr rmp ; Clear the complete mantissa space
ldi rCnt,MantLength
InitMant1:
st Z+,rmp ; Clear the mantissa
dec rCnt ; At the end?
brne InitMant1
sts sMant+2,rmp ; Set start value, Mantissa
ret

Note that both buffers are filled simultanously with the use of the STD instruction. At the end the two STS instructions set the 5 to the right place in both buffers. The init process has been executed in the simulator avr_sim. Both numbers are set to 0.5 now. The next step is to divide the decimal adder by two to get the adder for bit 13. Procedure starts with the 5 in the buffer and proceeds over the whole buffer length. If the division by two leaves a remainder (as is already the case with the first digit 5 / 2 = 2, remainder = 1), 10 has to be added to the next digit. The division by two is a simple task, as the whole algorithm goes like this:

; Divide the adder by two
DivideBy2:
clc ; Clear carry for overflows
ldi rCnt,MantLength-2 ; Mantissa length minus one to counter
DivideBy2a:
brcc DivideBy2b ; Carry is not set, don't add 10
DivideBy2b:
lsr rmp ; Divide by two
st Z+,rmp ; Store division result
dec rCnt ; Count down
brne DivideBy2a
lsr rmp ; Divide by 2
st Z,rmp
brcc Divideby2e
inc rmp ; Round last digit up
Divideby2c:
st Z,rmp ; Correct last digit
subi rmp,10 ; Digit > 10?
brcs DivideBy2e ; Nope
DivideBy2d:
st Z,rmp ; Correct last digit
ld rmp,-Z ; Read pre-last digit
inc rmp ; Increase digit
st Z,rmp ; and store
subi rmp,10 ; Check digit >= 10
brcc DivideBy2d ; Yes, repeat
DivideBy2e:
ret  These show the simulation of the first two divisions of the adder.

The special here is to increase the last digit if the division of the last digit shifted a one out to carry. In that case the last digit (as accessible with ld R16,-Z) has to be increased. If that yields equals or more than ten (subi R16,10 does not set the carry flag), the overflow has to go back to the previous byte. This has to be repeated with all previous digits until the INC does not lead to a digit reaching or exceeding 10 any more. To the upper right is the next step seen: if the respective mantissa bit is one, the divided adder has to be added to the decimal mantissa. When adding, the procedure starts at the end of the mantissa and adder buffer and proceeds to the left of the buffer. Each digit has to be checked if, by adding the adder byte and the carry, the 10 has been reached or exceeded. If so, ten has to be subtracted and this overflow has to be added to the next digit.

If the mantissa bit is not one, then the next division takes place without adding. The source code for this is here:

ldi ZH,High(sMantEnd) ; Point Z to decimal mantissa, MSB
ldi ZL,Low(sMantEnd) ; dto., LSB
ldi rCnt,MantLength-1 ; Mantissa length to R16
ld rmp,-Z ; Read last mantissa byte
st Z,rmp ; Store in SRAM
subi rmp,10 ; Subtract 10
brcs MantAdd2 ; Carry set, smaller than 10
st Z,rmp ; Overwrite digit
sec ; Set carry for next digit
clc ; Clear carry for next adding
dec rCnt ; Count down
brne MantAdd1 ; Not yet complete
ret This shows the treatment of all 15 bits of the mantissa: dividing in any case, and adding only if the mantissa bit is one. Here shown for a mantissa of 0x5555, where every second bit is set one.

Converting the exponent bits These are the three components we need for handling the exponent:
1. the mantissa, as derived previously,
2. the decimal exponent, that starts with zero and increases or decreases when applying the binary exponent, and
3. the binary exponent that is to be applied to the mantissa, that can be between -128 and +127, in our example it is four.
First of all: the decimal mantissa is not normalized: its first digit is a zero and should be a non-zero number. The routine Normalize: normalizes this number:
• If the overflow-digit in position 0 is not zero, it shifts the complete number once to the right.
• If the first decimal digit in position 1 is zero, it shifts the complete number one or more positions to the left.
Each shifting changes the decimal exponent accordingly: shifts to the right increase the exponent while shifts to the left decrease it. This shows the first normalization: a shift to the left. Note that the decimal exponent has now become negative (bit 7 is one).

The source code for normalization:

; Normalize the decimal mantissa
Normalize:
lds rmp,sMant ; Read mantissa overflow byte
tst rmp ; Not zero?
brne NormalizeRight ; Shift to the right
Normalize1:
lds rmp,sMant+1 ; Read the first digit
tst rmp ; Zero?
breq NormalizeLeft ; If yes, shift left
ret ; No normalization necessary
; Shift exponent one position left
NormalizeLeft:
ldi ZH,High(sMant+1) ; Point to first digit, MSB
ldi ZL,Low(sMant+1) ; dto., LSB
ldi rCnt,MantLength-2 ; Shift counter
NormalizeLeft1:
ldd rmp,Z+1 ; Read the next byte
st Z+,rmp ; Copy it to the current position
dec rCnt ; Count down
brne NormalizeLeft1 ; Additional bytes to move
clr rmp ; Clear the last digit
st Z,rmp ; in the last buffer
dec rDecExp ; Decrease decimal exponent
rjmp Normalize1 ; Check if further shifts necessary ; Shift number to the right
NormalizeRight:
ldi ZH,High(sMantEnd-1) ; Place Z to the end, MSB
ldi ZL,Low(sMantEnd-1) ; dto., LSB
ldi rCnt,MantLength-1 ; Counter for digits
NormalizeRight1:
ld rmp,-Z ; Read digit left
std Z+1,rmp ; Store one position to the right
dec rCnt ; Count down
brne NormalizeRight1 ; Furchter digits
clr rmp ; Clear the first digit (overflow digit)
st Z,rmp
inc rDecExp ; Increase decimal exponent
ret

The decimal mantissa has been shifted one position to the left and is now normalized. As the binary exponent is four, now the mantissa has to be multiplied by two. This drecreases the binary exponent by one.

The source code for multiplication by 2 is the following:

; Multiply number by 2
Multiply2:
ldi ZH,High(sMantEnd) ; Z to end of mantissa, MSB
ldi ZL,Low(sMantEnd) ; dto., LSB
ldi rCnt,MantLength ; Over the complete length
clc ; No carry on start
Multiply2a:
ld rmp,-Z ; Read last digit
rol rmp ; Multiply by 2 and add carry
st Z,rmp ; Overwrite last digit
subi rmp,10 ; Subtract 10
brcs Multiply2b ; Carry set, smaller than 10
st Z,rmp ; Overwrite last digit
sec ; Set carry for next higher digit
rjmp Multiply2c ; To count down
Multiply2b:
clc ; Clear carry for next higher digit
Multiply2c:
dec rCnt ; Count down
brne Multiply2a ; Further digits to process
ret This is the simulated multiplication by 2. Note that the overflow byte is at one now, so following each multiplication a check whether another normalization has to be performed. If so, a right-shift is performed to normalize the decimal mantissa again.

The same happens if the binary exponent is negative (bit 7 = one). In that case the mantissa has to divided by two and the normaization check should repair any losses of the first digit, by shifting the mantissa one or more positions to the left.

These steps are repeated until the binary exponent reaches zero.

Rounding the decimal mantissa We reserved the three last (insignificant) digits for the repeated shifting in the previous phase, but now we use them for rounding the result. To do that we add 0.00000555 to our interim result. This should round these three digits sufficiently.

; Round the mantissa up
RoundUp:
ldi ZH,High(sMantEnd)
ldi ZL,Low(sMantEnd)
ldi rmp2,5
ldi rCnt,3
clc
RoundUp1:
ld rmp,-Z
st Z,rmp
subi rmp,10
brcs RoundUp2
st Z,rmp
sec
rjmp RoundUp3
RoundUp2:
clc
RoundUp3:
dec rCnt
brne RoundUp1
ldi rmp2,0
ldi rCnt,MantLength-3
RoundUp4:
ld rmp,-Z
st Z,rmp
subi rmp,10
brcs RoundUpRet
dec rCnt
brne RoundUp4
rcall Normalize
RoundUpRet:
ret

Note that, under rare circumstances, rounding can lead to an overflow even to the byte 0. Therefore we finally have to check if an additional normalization is necessary. This is not the case if the up-rounding chain ends already in a lower byte position.

With that we have our float now complete for its conversion to ASCII format.

Conversion from BCD to ASCII All numbers in our decimal are BCDs. We have to add 0x30 (or subtract -'0') to get ASCII characters. Of course we'll have to add
1. the sign of the decimal mantissa, if it is negative (a blank if otherwise),
2. the decimal dot,
3. if the decimal exponent is not zero, we'll have to add E, the sign of the decimal exponent, and the exponent in two-digit format. If not we add four blanks.

Execution times

If you are short in time, because your AVR has more urgent things to do than converting floats to decimals: here are the execution times.

The complete procedure needs roughly the following times:

MantissaExponentDuration
0x40000x00448 µs
0x01668 µs
0x02816 µs
0x102.15 ms
0x7F23.2 ms
0xFFFF0x00449 µs
0x55552.88 ms
0x7FFF3.87 ms

The cases with negative mantissas or exponents are not differing much from the postive cases as there are only two additional instructions (a NEG and a COM).

If you need the assembler source code (419 lines) for own experiments or extensions to 32/40/48/56/64 bit floats, here is it: float_conv16.asm".

Faster than above: converting a 40-bit-binary to decimal

This above was not very effective because we used lots of slow SRAM and used a whole byte per decimal digit. The following shows the more effective way to do conversion of a 40-bit-binary, consisting of 32 bits mantissa and 8 bits exponent, to a decimal. With the above method this would last at least 50 ms, so we need a faster method for this.

We do that in the following way:
1. It first converts the 32-bit mantissa to an integer value. Because a 32 bit binary can hold decimal numbers of up to 4 billion and hence with 10 digits accuracy, we need an integer that can hold up to five bytes, but as we will have some overflow during multiplications, we use six bytes. For each of the 32 mantissa bits, the decimal representation of the weight of this bit is added to the result. Again, like demonstrated above, we start with 0.5, which is decimal 50.000.000.000 or hexadecimal 0B,A4,3B,74,00. These five bytes are repeatedly divided by two to get the next bit's weight factor as decimal. If the mantissa bit is one, the decimal is added to the result in rAdd5:4:3:2:1:0.
2. The integer is then multiplied with the exponent: each positive exponent multiplies the integer by two. If the left-shift shifts a one to byte 6 in rAdd5, the number is divided by 10 and the decimal exponent is increased by one. If the exponent is negative, the number in rAdd5:4:3:2:1:0 is divided by two. If rAdd4 gets empty by shifting, the number is multiplied by 10 (*4 by two left shifts, *5 plus original value, *10 by an additional left shift and the decimal exponent is decreased by one.
3. If follows normalization of the decimal integer: If byte 6 in rAdd5 is not zero, the number is divided by 10 and the decimal exponent is increased. If the number in rAdd4:3:2:1:0 is larger than or equal to 1.000.000.000.000 or 0xE8.D4.A5.10.00 (the maximum integer that the following integer-to-decimal conversion can handle), the number is also divided by 10 and the decimal exponent is increased. In case that the number did not exceed the maximum, it is checked whether it is smaller than 100.000.000.000 or 0x17.48.76.E8.00. If that is the case, the number is multiplied by 10 and the decimal exponent is decreased. That ensures that the first digit is at least a one (normalization of the decimal).
4. The integer is then converted to a decimal value. This is done by subtracting 100.000.000.000 repeatedly from the integer until an underflow occurs. This leads to the first digit and the decimal subtractor is added again. The decimal dot then follows. The following digits are derived by repeatedly subtracting the next lower decade, and down until 10. The last digit is the rest of the number. Note that dividing a 6-byte integer by 10 requires shifting the 48 bits bitwise to the left into another register. If that gets larger or equal 10, a one is shifted into the result, if not a zero is shifted. The division routine is a bit lengthy and consumes lots of execution time. As this routine is repeatedly executed if large positive binary exponents have to be processed, their time consumption is higher than for all other cases. But: compare these times with the ones above and consider that we have doubled the mantissa bits (from 16 to 32).

The table on the right shows the results for various input combinations.

The source code in assembler format can be downloaded from here. If you like to use it for serious applications: add another byte to the right to get increased accuracy and reduce the Div10 routine from 48 down to 40 bits in cases where no overflow is in rAdd5 to increase the execution speed.

Conclusion

Keep away from those fractional numbers. They eat your performance and blow up your code with, in most cases, completely unneeded trash.

To the page top