Path: Home => AVR-EN => assembler introduction => Floating point numbers => Conversion to decimal    (Diese Seite in Deutsch: Flag DE) Logo

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:

Format of the decimal result 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 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 
sAdder:
  .byte MantLength
sAdderEnd:
  .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 The decimal mantissa, and also the adder space, with its eight bytes each now look like this.

Converting the mantissa to decimal

Adding the 14th bit 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:
  std Z+dAddMant,rmp ; Clear the adder
  st Z+,rmp ; Clear the mantissa
  dec rCnt ; At the end?
  brne InitMant1
  ldi rmp,5 ; Start with bit 15
  sts sMant+2,rmp ; Set start value, Mantissa
  sts sAdder+2,rmp ; and adder
  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.

Simulating Init The init process has been executed in the simulator avr_sim. Both numbers are set to 0.5 now.

Dividing the adder by two 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:
  ldi ZH,High(sAdder+1) ; Point to end of the adder, MSB
  ldi ZL,Low(sAdder+1)
  clc ; Clear carry for overflows
  ldi rCnt,MantLength-2 ; Mantissa length minus one to counter
DivideBy2a:
  ld rmp,Z ; Read byte from adder
  brcc DivideBy2b ; Carry is not set, don't add 10
  subi rmp,-10 ; Add ten
DivideBy2b:
  lsr rmp ; Divide by two
  st Z+,rmp ; Store division result
  dec rCnt ; Count down
  brne DivideBy2a
  ld rmp,Z ; Read last byte from adder
  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

Dividing 0.5 by 2 for bit adder 13

Dividing 0.25 by 2 for bit adder 12 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.

Adding the adder for bit 12 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:

; Add the adder to the decimal mantissa
MantAdd:
  ldi ZH,High(sMantEnd) ; Point Z to decimal mantissa, MSB
  ldi ZL,Low(sMantEnd) ; dto., LSB
  ldi rCnt,MantLength-1 ; Mantissa length to R16
  clc ; Start with carry clear
MantAdd1:
  ld rmp,-Z ; Read last mantissa byte
  ldd rmp2,Z+dAddMant ; Read corresponding adder byte
  adc rmp,rmp2 ; Add both with carry
  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
  rjmp MantAdd3 ; Count down
MantAdd2:
  clc ; Clear carry for next adding
MantAdd3:
  dec rCnt ; Count down
  brne MantAdd1 ; Not yet complete
  ret



Dividing and adding 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

The input parameters for exponent handling 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: Each shifting changes the decimal exponent accordingly: shifts to the right increase the exponent while shifts to the left decrease it.

Normalizing by shifting to the left 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

Simulating normalization of the mantissa

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

Multiplcation by two, binary exponent minus one 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

Multiplication of decimal mantissa by 2 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

Rounding of the result 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
  adc rmp,rmp2
  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
  adc rmp,rmp2
  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

Conversion to an ASCII string 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.
Execution results and times 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

©2021 by http://www.avr-asm-tutorial.net