Binary to Decimal Conversion in Limited Precision

来源:互联网 发布:崛起罗马之子优化好吗 编辑:程序博客网 时间:2024/04/29 15:41

 

Binary to Decimal Conversion in Limited Precision

Part of the Arithmetic Tutorial Collection
by Douglas W. Jones
THE UNIVERSITY OF IOWA Department of Computer Science

Copyright © 1999, Douglas. W. Jones, with major revisions made in 2002 and a small update in 2007. This work may be transmitted or stored in electronic form on any computer attached to the Internet or World Wide Web so long as this notice is included in the copy. Individuals may make single copies for their own use. All other rights are reserved.

Index

  • Introduction
  • The Basic Idea
  • Reduction to Code
  • Dealing with Signed Numbers
  • Living Within 8 Bits
  • Doing Without Hardware Multiply and Divide
Rated as a top 50 site by Programmer's Heaven as of Feb 2002.

Introduction

Programmers on binary computers have always faced computational problemswhen dealing with textual input-output because of our convention that printednumeric data should be represented in base 10. These problems have neverbeen insurmountable, but they are particularly annoying when writing programsfor machines with limited capacity in their arithmetic units. Suppose, forexample, you have an unsigned binary number that you wish to print out indecimal form. The following C code will do this:

    void putdec( unsigned int n )
{
unsigned int rem = n % 10;
unsigned int quot = n / 10;
if (quot > 0) putdec( quot );
putchar( rem + '0' );
}

The problem with this C code is that it assumes that the computer's arithmeticunit is sufficiently large to handle the entire number n, and it assumes thatthe arithmetic unit can conveniently do integer division yielding both aquotient and a remainder. These assumptions are not always reasonable, bothbecause many commercially successful computers do not include divisionhardware and because, even when such hardware is available, it is of littledirect use when dividing numbers larger than the ALU can handle.

The focus of this tutorial is on writing fast code to convert unsigned 16 bitbinary numbers to decimal on a machine with an 8-bit ALU and no divideinstruction. This situation is common on small microcontrollers, and thetechniques generalize in useful ways, for example, to the problem of doing64 bit binary to decimal conversion using a 32 bit ALU.

On machines with no divide instruction where speed is no issue, it maybe better to simply use repeated subtraction instead of the fast butalgebraically complex code given in the body of this tutorial.Consider the following 16-bit conversion code as an example:

    void putdec( int n )
{
char a, b, c, d;
if (n < 0) {
putchar( '-' );
n = -n;
}
for ( a = '0' - 1; n >= 0; n -= 10000 ) ++a;
for ( b = '9' + 1; n < 0; n += 1000 ) --b;
for ( c = '0' - 1; n >= 0; n -= 100 ) ++c;
for ( d = '9' + 1; n < 0; n += 10 ) --d;
putchar( a );
putchar( b );
putchar( c );
putchar( d );
putchar( n + '0' );
}

Variants on the clever algorithm given above have been very popular on8-bit microprocessors and microcontrollers, despite the fact that conversionscan require over 30 iterations of the for loops in this code.

 


The Basic Idea

To do the conversion faster, what we'll do is look at our binary numberas a base 16 number and then do the conversion in terms of the base 16digits.Consider a 16 bit number n. This can be broken into 4 fields of4 bits each, the base 16 digits; call them n3,n2, n1 and n0.

                    n
|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|
| n | n | n | n |
3 2 1 0

The value of the number can be expressed in terms of these 4 fields asfollows:

n = 4096n3 + 256n2 + 16n1 + n0

In the same way, if the value of n, expressed in decimal, isd4d3d2d1d0, where each ofd4, d3, d2,d1 and d0 are decimal digits, thevalue of n can be expressed as:

n = 10000d4 + 1000d3 + 100d2 + 10d1 + d0

Our problem then, is to compute the di from theni without dealing directly with the largernumber n.

To do this, we first note that the factors used in combining the values ofni can themselves be expressed as sums of multiples ofpowers of 10. Therefore, we can rewrite the original expression as:

n =
n3( 4×1000 + 0×100 + 9×10 + 6×1 ) +
n2( 2×100 + 5×10 + 6×1 ) +
n1( 1×10 + 6×1 ) +
n0( 1×1 )

If distribute the ni over the expressions for each factor,then factor out the multiples of 100, we get the following:

n =
1000(4n3) +
100(0n3 + 2n2) +
10(9n3 + 5n2 + 1n1) +
1(6n3 + 6n2 + 6n1 + 1n0)

We can use this to arrive at first approximations aifor d3 through d0:

a3 = 4n3
a2 = 0n3 + 2n2
a1 = 9n3 + 5n2 + 1n1
a0 = 6n3 + 6n2 + 6n1 + 1n0

The values of ai are not proper decimal digits becausethey are not properly bounded in the range0 < ai < 9.Instead, given that eachof ni is bounded in the range0 < ni < 15,the aiare bounded as follows:

0 < a3 < 60
0 < a2 < 30
0 < a1 < 225
0 < a0 < 285

This is actually quite promising because a3 througha1 are less than 256 and may therefore be computed usingan 8 bit ALU, and even a0 may be computed in 8 bits if wecan use the carry-out bit of the ALU to hold the high bit. Furthermore,if our interest is 2's complement arithmetic where the high bit is thesign bit, the first step in the output process is to print a minus signand then negate, so the constraint on n3 becomes0 < n3 < 8and we can conclude naively that

0 < a0 < 243

Note that we saidn3 < 8and notn3 < 8;this is because of the one negative number in the 2's complement systemthat cannot be properly negated, -32768. If we exclude this number fromthe range of legal values, the upper boundis reduced to 237; in fact, this is the overall upper bound, becausein the one case wheren3 is 8, all of the otherni are zero, so we can assert globally that

0 < a0 < 237

Even with our naive bound, however, it is easy to see that we cansafely use 8 bit arithmetic to accumulate all of theai.

Given that we have computed the ai, we can push theminto the correct ranges by a series of 8-bit divisions and one 9-bit division,as follows:

c1 = a0 / 10
d0 = a0 mod 10

c2 = (a1 + c1) / 10
d1 = (a1 + c1) mod 10

c3 = (a2 + c2) / 10
d2 = (a2 + c2) mod 10

c4 = (a3 + c3) / 10
d3 = (a3 + c3) mod 10

d4 = c4

In the above, the ci terms represent carries propagatedfrom one digit position to the next. The upper bounds on each of theci can be computed from the bounds given above for theai:

c1 < 28 [ = 285/10 ]
c2 < 25 [ = (28 + 225)/10 = 253/10 ]
c3 < 5 [ = (25 + 30)/10 = 55/10 ]
c4 < 6 [ = (5 + 60)/10 = 65/10 ]

In computing the bound on c2, we came within 2 ofto 255, the maximum that can be represented in 8 bits, so an 8 bitALU is just barely sufficient for this computation. Again, if wenegate any negative numbers prior to output, so that the maximumvalue of n3 is 8, the bound on c1becomes 23 instead of 28.

 


Reduction to Code

A first hack at reducing the above idea to code might include named temporariesfor all of the terms used above, but we can avoid this if we reorganizethings slightly and note that, after computing each digit of the number, weno-longer need one of the ni. This leads to thefollowing C code, assuming that C allocates 16 bits to items of typeshort int and 8 bits to items of type char:

    void putdec( unsigned short int n )
{
unsigned char d4, d3, d2, d1, q;
unsigned short int d0;

d0 = n & 0xF;
d1 = (n>>4) & 0xF;
d2 = (n>>8) & 0xF;
d3 = (n>>12) & 0xF;

d0 = 6*(d3 + d2 + d1) + d0;
q = d0 / 10;
d0 = d0 % 10;

d1 = q + 9*d3 + 5*d2 + d1;
q = d1 / 10;
d1 = d1 % 10;

d2 = q + 2*d2;
q = d2 / 10;
d2 = d2 % 10;

d3 = q + 4*d3;
q = d3 / 10;
d3 = d3 % 10;

d4 = q;

putchar( d4 + '0' );
putchar( d3 + '0' );
putchar( d2 + '0' );
putchar( d1 + '0' );
putchar( d0 + '0' );
}

Of course, the above code prints all 5 digits of n instead of onlyprinting the significant digits, but this can be fixed in many ways. Thefollowing fix is perhaps the cleanest, making effective use of partialresults as soon as those results can be used to predict that digits willbe zero. Even so, adding these features to the code obscures thecomputation being performed.

    void putdec( unsigned short int n )
{
unsigned char d4, d3, d2, d1, q;
unsigned short int d0;

d0 = n & 0xF;
d1 = (n>>4) & 0xF;
d2 = (n>>8) & 0xF;
d3 = (n>>12);

d0 = 6*(d3 + d2 + d1) + d0;
q = d0 / 10;
d0 = d0 % 10;

d1 = q + 9*d3 + 5*d2 + d1;
if (d1 != 0) {
q = d1 / 10;
d1 = d1 % 10;

d2 = q + 2*d2;
if ((d2 != 0) || (d3 != 0)) {
q = d2 / 10;
d2 = d2 % 10;

d3 = q + 4*d3;
if (d3 != 0) {
q = d3 / 10;
d3 = d3 % 10;

d4 = q;

if (d4 != 0) {
putchar( d4 + '0' );
}
putchar( d3 + '0' );
}
putchar( d2 + '0' );
}
putchar( d1 + '0' );
}
putchar( d0 + '0' );
}

Dealing with Signed Numbers

To print signed values, we can replace the first few lines of the abovecode with the following:

    void putdec( int n )
{
unsigned char d4, d3, d2, d1, d0, q;

if (n < 0) {
putchar( '-' );
n = -n;
}

d0 = n & 0xF;
d1 = (n>>4) & 0xF;
d2 = (n>>8) & 0xF;
d3 = (n>>12) & 0xF;

/* the remainder of the code is unchanged */

As mentioned earlier, forcing n positive prior to breaking it downinto nibbles allows us to use a char type for d0 insteadof a short int type.

Note that the computation of d3 has also changed as a result ofthe change from unsigned to signed arithmetic. This is because theright-shift operator shifts zeros into the high bit when applied tounsigned numbers, but it replicates the sign bit when applied to signednumbers. There is one case in 16 bit 2's complement arithmetic whereboth n and -n are negative, that is, n = -32768.The change we have made gives us the correct output in this case.

 


Living Within 8 Bits

In the code given above for unsigned printing, the fact thata0 could be as large as 285 forced us to declare thevariable d0 as short int instead of char.We can avoid this by observing that the final addition in the sumused to compute a0 is the only one that will pushthis sum over 255, and therefore, we need only check the carry bit onceto detect overflow. Having detected overflow, we can account for it inthe computation that follows:

    void putdec( unsigned short int n )
{
unsigned char d4, d3, d2, d1, d0, q;

d0 = n & 0xF;
d1 = (n>>4) & 0xF;
d2 = (n>>8) & 0xF;
d3 = (n>>12) & 0xF;

if ( (6*(d3 + d2 + d1) + d0) > 255 ) { /* overflow */
d0 = (6*(d3 + d2 + d1) + d0) & 0xFF;
q = d0/10 + 25;
d0 = d0 % 10 + 6;
if (d0 >= 10) {
d0 = d0 - 10;
q = q + 1;
}
} else { /* no overflow */
d0 = 6*(d3 + d2 + d1) + d0;
q = d0 / 10;
d0 = d0 % 10;
}

In the above code, after having noted an overflow in the computation ofa0, we retain the least significant bits of the sumin d0 and then approximately correct the quotient byadding 256/10, or 25; correcting this and computing the correctremainder is a bit more interesting.

Given some integer i > 256, if we want to computei mod 10 from (i - 256) mod 10, we need to note thatthe modulus operator is distributive in an interesting sense:

(i + j) mod m =((i mod m) + (j mod m)) mod m

Using this, we can conclude that:

i mod 10 = (((i - 256) mod 10) + (256 mod 10)) mod 10
= (((i - 256) mod 10) + 6) mod 10

In the case where ((i - 256) mod 10) + 6 is greater than 10,the truncation of the quotient was also incorrect, so we must add 1to the approximate quotient as well. This justifies the code given above,and this code has also been tested exhaustively for all integers from0 to 65535.

 


Doing Without Hardware Multiply and Divide

If flat-out speed is not of the essence, the easiest way to do therequired divisions is to use the trivial repeated subtraction algorithm.The largest dividend we must consider is only 285, so it will take nomore than 28 iterations for this division.

    /* quotient and remainder returned by div10 */
unsigned char q, r;

void div10( unsigned char i )
{
q = 0;
r = i;
while (r > 9) {
q = q + 1;
r = r - 10;
}
}

This can be optimized in several ways. Most notably, termination conditionsthat compare with zero are almost always significantly faster than thosethat compare with other constants, so we might write something like this:

    /* quotient and remainder returned by div10 */
unsigned char q, r;

void div10( unsigned char i )
{
q = -1;
r = i;
do {
q = q + 1;
r = r - 10;
} while (r >= 0);
r = r + 10;
}

Another section of this tutorialcovers the problem of doing multiplicaton and division using only shiftand add instructions. We use the results presented there, taking specificadvantage of the limited ranges of values occupied by our dividends, tocompletely eliminate iteration from our code.

There are two ways to get an exact quotient and remainder fromi/10 using reciprocal multiplication in 8 bits.Either we multiply by 0.00011001 to get an approximate quotient and thencompute the remainder and correct the quotient, or we multiply by0.00011001101 to get an exact quotient from which we can compute theremainder.

The former method, yielding an approximation, can be reduced to thefollowing C code:

    /* quotient and remainder returned by div10 */
unsigned char q, r;

void div10( unsigned char i )
{
/* approximate the quotient as q = i*0.000110011 (binary) */
q = ((i>>1) + i) >> 1; /* times 0.11 */
q = ((q>>4) + q) >> 3; /* times 0.000110011 */

/* compute the reimainder as r = i - 10*q */
r = ((q<<2) + q) << 1; /* times 1010. */
r = i - r;

/* fixup if approximate remainder out of bounds */
if (r >= 10) {
r = r - 10;
q = q + 1;
}
}

In first two right-shift-and-add statements used above, we haveassumed that the carry out of the high bit of the add operationis shifted into the high bit of the second of the two right-shiftoperations used in each statement. This can be done efficientlyin many machine instruction sets, but even good optimizing compilersmay insist on using a 16 bit accumulator for all intermediateresults of arithmetic operations.

The second approach, using a more elaborate multiplier to give anexact quotient, may be coded as follows:

    /* quotient and remainder returned by div10 */
unsigned char q, r;

void div10( unsigned char i )
{
/* approximate the quotient as q = i*0.00011001101 (binary) */
q = ((i>>2) + i) >> 1; /* times 0.101 */
q = ((q ) + i) >> 1; /* times 0.1101 */
q = ((q>>2) + i) >> 1; /* times 0.1001101 */
q = ((q ) + i) >> 4; /* times 0.00011001101 */

/* compute the reimainder as r = i - 10*q */
r = ((q<<2) + q) << 1; /* times 1010. */
r = i - r;
}

The particular choice of one or the other of these schemes can only bemade after compiling them or hand translation to machine code. Dependingon the specific features offered by the target architecture, either of theabove may be the fastest or most compact, and careful use of smalloptimizations may tilt the balance one way or the other.

Depending on the space-time tradeoffs of the application, the abovecode may be expanded in-line as an open macro, or it may be called as aclosed function. If in-line expansion is appropriate, it should be notedthat the computations of c3 and c4involve dividends no greater than 65. For dividends up to 68, themultiplier 0.0001101 gives exact results, suggesting the following versionof the print routine:

    void putdec( short int n )
{
unsigned char d4, d3, d2, d1, d0, q;

if (n < 0) {
putchar( '-' );
n = -n;
}

d1 = (n>>4) & 0xF;
d2 = (n>>8) & 0xF;
d3 = (n>>12) & 0xF;

d0 = 6*(d3 + d2 + d1) + (n & 0xF);
q = (d0 * 0xCD) >> 11;
d0 = d0 - 10*q;

d1 = q + 9*d3 + 5*d2 + d1;
q = (d1 * 0xCD) >> 11;
d1 = d1 - 10*q;

d2 = q + 2*d2;
q = (d2 * 0x1A) >> 8;
d2 = d2 - 10*q;

d3 = q + 4*d3;
d4 = (d3 * 0x1A) >> 8;
d3 = d3 - 10*d4;

putchar( d4 + '0' );
putchar( d3 + '0' );
putchar( d2 + '0' );
putchar( d1 + '0' );
putchar( d0 + '0' );
}

All multiplications in the above code can be reduced to short sequencesof shift and add instructions, and it is straightforward to add the codediscussed earlier to suppress leading zeros from this code.

All of these details are incorporated into the attacheddecimal print routine for the 14-bit family of PIC microcontrollers.

 

Thanks to Mark Ramsey, who, on January 22, 2007 pointed out two small typoswith big consequences in the section on Living Within 8 Bits.

Thanks to Dennis Vlasenko, who, on June 15, 2007 pointed out more typos inthe section on Doing Without Multiply and Divide.

Thanks to Joe Zbiciak, who, on August 23, 2007 pointed out a small errorthe introduction.

原创粉丝点击