SLTF Consulting
Technology with Business Sense

 


 

Don't Forget Bit Basics While Using an HLL

Scott Rosenthal
September, 1992

In my opinion, computer programming is nothing more than the manipulation of numbers. Programmers programmed the first computers by entering series of numbers which in turn instructed the computer on how to manipulate other numbers. Even though some people call these numbers "data", the computer contains nothing but bits (ones and zeros) that we humans group into meaningful information. Computers use numbers to represent abstract ideas. A simple example of this is alphanumerics. You can't directly express the letter 'B' in a computer but you can instead use a numeric representation such as ASCII (0x42 or 42 Hex). Embedded systems use numbers to represent the amplitude of an audio signal, the depth of the water beneath the keel of a sailboat, and the manifold pressure in an automobile. Numbers are essential to programming, especially embedded systems, that I think it would be very worthwhile to explore various techniques that will make it easier to write program code and at the same time reduce code size and execution time.

Bits and Bytes and Words, Oh My!

Just so that we all use the same terminology, let me review some of the basic number representations used in embedded systems. A Bit is the smallest information unit in a computer. It represents, in computer speak, either a 1 or a 0, which also happens to be the only two digits in the binary or base two number system. (Mike, I know that the following is passive voice but I couldn't find any other way that worked as well) This was drilled into me by my sixth grade teacher (thank you, Mr. Campbell) who spent a half hour switching the room lights off then on and asking, "Why is binary used in computers?" We weren't dumb but sometimes the obvious just seems too simple!

Binary is an inconvenient numbering system in which to express larger numbers. As an example, take the number 987 and convert it into binary: 1111011011. We humans tend to confuse long strings of numbers and look for ways to group the information into manageable chunks; we arrange our 7 digit telephone numbers as groups of three and four, and we group our nine digit Social Security Numbers into groups of three, two, and four. If we regroup this binary number into groups of three, it now looks like this: 1 111 011 011. This is easier but we can make it even simpler. The grouping by threes is really an octal (base eight) grouping and we can now rewrite the number as 1733Q (Q means octal).

Many memory systems group bits together into multiples of eight called bytes. You can use octal to represent the values of these bits and it will take 3 digits (two groups of three and one group of two). A more convenient grouping is by fours, which requires just two digits. This is hexadecimal, base 16 (also known as hex). Hexadecimal uses the digits 0 to 9 and A to F to represent numbers from 0 to 15 (four bits). We can now write our 987 as 3DBH (H means hex) or in C terminology as 0x3db. The point to keep in mind is that octal and hex are just representations of the same binary pattern and any action to any one bit of the number will change that value in every numbering system. Decimal has no easy translation between a bit change and a number and you should entirely avoid using it when programming embedded systems. Don't extend this though to the user of your system! Most normal people don't think in computerese so you'll have to translate these INTERNAL number representations into normal, everyday usage. Also, even though computers can start counting from zero, don't make your customer also count this way. People start counting from one. Always! Enough of the simplicity.

Bit Juggling

Now that we know that all numbers in computers are just sequences of bits, we can use this information to make our number manipulation chores easier than normal math allows. Computers are generally very slow in doing multiplications and divisions and therefore you should try to avoid these instructions whenever possible. Most computers allow operations on the individual bits of a number such as setting and clearing. For a computer without direct bit control, the 'or' and 'and' operations are synonymous. Using the ASCII character set as an example, you can convert an upper case letter, such as 'B', to its lower case equivalent, just by setting the sixth bit. This converts the ASCII number for 'B', 0x42, to its lower case value of 0x62. This is one of the ways that you can implement the C function tolower():

#define tolower(c) (c | 0x20)

You can easily relegate other tasks to bit manipulations. Odd and even numbers are a very simple case. Even numbers are those integers that are evenly divisible by two. You can easily test to see if a number is even by checking the first bit. If it's set, the number's odd and if it's not set, then the number is even. The standard way you would round an odd number up to the next even number might be by using the following code snippet:

if (number & 1) number++;

This technique, when translated to its assembly language, requires a test, a conditional jump, and an increment instruction. You can shorten this by a third using the following:

number = (++number) & ~1;

This technique works by incrementing the number, which causes the odd number to become even and the even number to become odd. After incrementing, the number has the first bit turned off by the 'and'. This doesn't require a conditional jump and will always take the same time to execute, unlike the first way. Real-time data taking is generally very intolerant of varying execution times; adopting programming techniques that require fixed periods of time will generally give you a better design with fewer quirks and problems.

Most language compilers support some type of modulo function that returns the remainder for some divisor. This is analogous to the clock math we learned in elementary school. A typical way that you can implement the modulo function is the following:

#define modulo(number, divisor) \

((number) - (((number) / (divisor)) * (divisor)) )

If your task allows the divisor to be a power of 2 (e.g., 2, 4, 8, 16, etc.) then the problem simplifies to just bit manipulation:

#define modulo(number, divisor) ((number) & ((divisor) - 1))

This is not only faster executing but it also requires less program memory.

As I said before, an embedded system's programmer must know how to juggle bits. But, even programmers on large systems can benefit from intimate dealings with the bits and bytes. I was recently porting a PC regression analysis program to a Cray-2 (surprisingly easy) and we came across a question about the arrangement of the Cray pointers. I needed this information to make sure that my program could access any of the eight bytes making up its basic word. I wrote a simple test program and looked at the pointer value. The default decimal numbers made no sense but redisplaying them in hex showed that the pointer used the upper three bits as the byte offset within the word. On numbers representing internal program values or hardware data, get yourself used to using the natural numbering systems of the computer -- binary, hex, and maybe even octal.

A lot of microprocessors and microcomputers don't have multiply and divide instructions and even those that do sometimes take an horrendous amount of time to perform these operations. For these micros that lack multiply and divide instructions, you'll need to either develop or purchase an integer math package. For simple multiplications, though, you'll find that specialized functions are much faster.

As you know, a multiplication by 2 is the same as shifting a binary number to the left by one bit. The number 3, written in binary as 0011, multiplied by 2 yields 6, binary 0110. Shifts execute fast on micros as do add instructions and you can use this shifting and adding to quickly perform multiplies of constant values.

Integer multiplication is really just a shortcut for multiple additions. Five times x is the same as x + x + x + x + x. You can regroup this as (4 * x) + x, which in our shift and add world is now simply two shifts of x to the left and one add of x. Most C compilers on a PC do simple constant multiplication in this manner. The following table shows you the combinations of shifts and adds (in C nomenclature) for the numbers two to ten:

x * 2 (x << 1)

x * 3 (x << 1) + x

x * 4 (x << 2)

x * 5 (x << 2) + x

x * 6 (((x << 1) + x) << 1)

x * 7 (((x << 1) + x) << 1) + x

x * 8 (x << 3)

x * 9 (x << 3) + x

x * 10 (((x << 2) + x) << 1)

These constant multiplications are convenient but just remember to make sure that you watch out for overflow problems.

Johnny, Remember to Expand Your Polynomials

A lot of the instruments and software I design involve higher-order mathematics and as such, the program is constantly calculating polynomials. For example, my program might be solving a third order polynomial of the form:

y = ax^3+ bx^2+cx+d

and the most obvious way to write this in C code is the following:

y = a * x * x * x + b * x * x + c * x + d; (1)

An even less experienced programmer might have written it like this:

y = a * pow(x, 3.0) + b * pow(x, 2.0) + c * x + d; (2)

As I said before, we want to minimize multiplies and divides wherever possible, especially if the above equation is in floating point! You can rearrange the above polynomial and rewrite it in the following form:

y = (((a) * x + b) * x + c) * x + d; (3)

You can see that (3) uses fewer math operations than (1); three multiplications and three additions compared to six multiplications and three additions. It also doesn't require any intermediate storage of each newly calculated term. Its number of multiplications and additions are always equal to the polynomial order while the code line (1) multiplications always equals the sum from 1 to the polynomial order. In one instrument I designed, there was a fifth order polynomial divided by another fifth order polynomial. If I had used (1), that would have been 30 multiplies, 10 additions, and one division. Using (3), my program had 10 multiplies, 10 additions, and one division, which is almost half the math operations! In other words, I doubled the speed of my calculations just by rearranging the equation.

In my experience, programs always need to run faster. Compilers are fine for translating some higher level ideas into instructions the computer can execute but they aren't a substitute for proper program coding. No compiler is going to rearrange (1) as (3) and likewise, no compiler will optimize a rounding function. PE&IN



Copyright © 1998-2014 SLTF Consulting, a division of SLTF Marine LLC. All rights reserved.