Saving Memory with Integer Scaling

With an abundance of RAM memory and the availability of floating point in the latest microcontrollers, there is little concern about minimizing memory for data storage and optimizing computation time. However, smaller microcontrollers have limited memory and no floating point, and it may become necessary to optimize the usage of memory.

Accuracy and Precision

Let’s first look at accuracy and precision. These terms are often used interchangeably, however their meanings are actually very different.

Accuracy refers to the amount of potential error in a value. It generally originates from sensor measurement errors, but the potential error will propagate through all calculations. The potential error is usually split up as the sum of two or more components. For our purposes, consider it to be of two types:

  • Absolute Error — a fixed amount. Think of a length being accurate to within ±0.1 inch.
  • Relative Error — the potential error being a percentage of the measured value. Think of a length being accurate to within 1% of the measured value. 

Note that if accuracy is quoted as being within a certain percentage of “full scale”, this is actually an absolute error and not a relative error specification. For instance if a meter is accurate within 1% of full scale,  and the meter reads 0 to 10 volts, then the reading is accurate to ±0.1 volt. 

Precision deals with the granularity of the value. For instance, if you are counting something, say golf balls, the granularity will be 1, and that is the maximum precision, within one golf ball. You can’t have .1 golf balls. My bathroom scale has a granularity (precision) of 0.2 pounds.  It will never indicate a weight ending in 0.1, 0.3, 0.5, 0.7 or 0.9.

Note that the scale may not be accurate to 0.2 pounds (or ±0.1 pounds). It could have an error of several pounds. A more precise value doesn’t imply a more accurate value!

Conversely, a more accurate value is “wasted” if the value cannot be represented with sufficient precision. If my bathroom scale was accurate to ±0.01 pounds, there would be no way to tell.

In a computer we are generally storing values in containers with a fixed number of bits. For example, let’s consider 8-bit values. Values are precise to the least significant bit. For unsigned, unscaled integers, the values are 0 through 255, precise to 1 unit, or ±½ unit. If we fully utilize the 8-bit value we are precise to 1 part in 256, which is 8 bits of precision. Note that to get that maximum precision we must be able to utilize all the bits in the storage value.

Floating Point

Values that are very large or very small can’t be well represented as integers without scaling. To scale, we multiply the value by a power of two to get it to fit in the storage location, hopefully using all the bits of the location to hold the value. This can be done automatically by using floating point values. A floating point value looks like this:

A fixed number of bits are allocated to hold the exponent of the scaling factor and the fraction. Except for some special values like zero, the fraction is a binary fraction with a leading “1.” assumed. So 1 <= F < 2. 

For the common IEEE 32-bit format, the fraction is 24 bits (including the leading 1) giving 24 bits of precision for any value that can be represented in this form. The exponent field is 8 bits in the range 1 to 254, with 0 and 255 reserved. The exponent is stored with an offset of 127, which means that a value of 127 means an exponent of zero.

The problems with floating point include that the minimum size value is 32 bits, and calculations have to be done in software (rather than hardware instructions) in most microcontroller, making slow as well as wasteful of memory.

Scaled Integers

A scaled integer works like a floating point number where the exponent is kept manually by the programmer. Only the sign and fraction are stored in memory. Let’s consider an 8 bit unsigned integer. It can have a value of 0 to 255. Its precision is ½ the least significant bit, or ±½ in this case. If we have a scale factor of 4, the value of the integer is 0 to 1020 and is precise to ±2.

We could store values in the range 0 to 1000 in 16-bit integers, but if we’re willing to accept a precision of ±2, we could store them in 8-bit bytes with a scale factor of 4. This would reduce the memory requirement by half.

When performing arithmetic operations on scaled integer values, it is crucial that the scale factors remain consistent. Typically, we adjust the value with the smaller scale factor to ensure that both values have the same scale factor. For multiplication and division, the scale factors are either multiplied or divided accordingly. It is essential to maintain track of the ranges of the values to prevent potential overflow errors.

Scaling does not have to be by powers of two. For example, lets say that our measured values are in the range 0 to 256. If we want to store values in 8 bit unsigned integers and use a scale factor of 2, the precision will be ±1. However if we use a scale factor of 256/255, the precision will be ±128/255, or almost that of an unscaled integer value. To store the measurement, we multiply by 255 and divide by 256. 

store_value = measurement * 255/256;

To use the value, we multiply by 256 and divide by 255.