Lecture: 30
Objective: To understand how to use a look-up table to interpolate values between table elements.
Excel: lut.xlsx

Look up Table (LUT)

Lets say that you wanted to compute the square root of a value using the PIC. If you were lucky enough to have a compiler which provided this function you could just use the math library functions. On the other hand, if you did not have the use of such a library, you would have to figure out a way to compute the square root.

There are a lot of ways that you could compute the SQRT function. You could crack open a math book and find an analytic expression which would produce a value for the SQRT function. Unfortunately, this approach often leads to timely computations. Another approach would be to enumerate every possible value of x and its square root. We could then look-up a SQRT in the table, by going to the row corresponding to x and retrieving its value. This approach seems silly because it would use a lot of space. If we reduced the size of the table by eliminating some of the entries then we would save space at the expense of introducing error - if you wanted the SQRT for x and its entry was not in the table then you would have to use the closest x in the table. A good compromise among all three of these design constraints (space, time, and error) is to use interpolation in a partial look-up table (LUT).

"Interpolation is a mathematical method of creating missing data. ... There are many methods of interpolation, but one simple method would be to generate a new value by using the average of the value of the two values on either side of the one to be created." This average is also referred to as linear interpolation.

Let's look at an example. In the left graph we have the plot of some hypothetical function f(x) shown in red. We will store every fourth value of f(x) at x = 0,4,8,12,16,20, and 24 and use these values to interpolate the values of f(x) for the other values of x. For example, let's say that you wanted to compute the value of f(7). To do this we will approximate the actual red function f(x) between x=4 and x=8 as a straight line between x=4 and x=8 shown in orange at right. This linear approximation forms the hypotenuse of the orange right triangle. To determine the value of f(7) we take a portion of the orange hypotenuse between x=4 and x=7 forming the hypotenuse of a smaller green right triangle.




To determine f(7) let's look at the orange triangle. Since we have stored the values of f(4) and f(8), we know the height of the orange right triangle equals f(8)-f(4). Second, we know the base length of the orange triangle because we stored every fourth sample of f(x). The base length is 4.

Now for the green triangle. It has a height of f(7)-f(4) and a base length of 3. Since the the orange and green triangles are similar triangles, the ratio of each triangle's height to base is equal. Doing the math provides the equality.
    f(8) - f(4)   f(7) - f(4)
    ----------  = ----------		Solving for f(7) yields
         4            3

f(7) = f(4) + 3/4(f(8)-f(4))

Firmware concepts

Let's take this expression for f(7), extract some general concepts and turn it into code. Let's start by storing the values of f(0), f(4), f(8), ... f(24) into an array. The next problem is how, given the value of 7, do we get the index of f(4) or f(8) to use in our calculation? Since we sampled the function f(x) at every fourth value, we need to know which "fourth value" is 7 closest to. This is the same as dividing 7 by 4 and discarding the fraction. 7/4 = 1.75 so we should use index 1 from the array of stored function values. This is f(4). Adding 1 to the index always produces the next stored value, in our case this is f(8). Looking at the expression for f(7) all we need is a way to compute the "3/4". This expression describes how far along through the "fourth value" we have traversed. This is the same as the fractional portion of 7/4 that we discarded earlier. In our case, this was 0.75 which is exactly 3/4.

The following firmware formalizes these observations. To compute the value of f(7) the function will have x = 7. I looked at the red graph in the plot above and made a guess at the values of f(x) at the seven stored points and came up with the values 10, 20, 100, 60, 40, 30 and 20. The first thing you see in the f function below is these values stored in an array func. I used uint8_t data type because all the function values are between 0 and 255.

The first thing that our code need to compute is the index into the func array. This is done by computing 7/4 and discarding the fraction. We can do this by shifting 7 right by 2 bits. I next computed the value of f(8) - f(4) and stored it in a variable delta. This code ignores the possibility that delta could be negative. For the most part this is not an issue as long as delta does not overflow and the output from the function is a positive value. Next, we need to compute the fraction 3/4. This is done in two separate steps, first we mask off the least two significant bits of x by ANDing with 0b00000011. This half of the fraction is stored in a variable frac. The other half of the computation, dividing by 4, is accomplished in the return statement when, after multiplying the delta value by frac, we divide by 4 (by shifting right 2 bits).
//----------------------------------------------
//----------------------------------------------
uint8_t f(uint8_t x) {

    uint8_t func[7] = {10, 20, 100, 60, 40, 30, 20};
    uint8_t index, delta, frac;

    index = x >> 2;
    delta = func[index+1] - func[index];
    frac = x & 0b00000011;

    return (func[index] + (frac*delta)>>2);
}

Square Root

Now lets examine how to determine a square root. The purple curve in the graph below shows the true value of the SQRT function for the integers 0-15. The yellow curve in the graph below represents the values of the SQRT function for 4 equally spaced values. These values are given in the table to the left of the graph.
x sqrt(x) 2.6 fixed point
0 0 0x00
4 2 0x80
8 2.828427125 0xB5
12 3.464101615 0xDE
16 4 0xFF


Firmware for square root

We will create a function to compute the square root using linear interpolation with fixed-point. We start by deciding on the fixed point representations The input argument is a 4-bit integer in the range 0-15. The output is a number between 0.0 and 3.99 and include some fractional part. Since 8-bit values are the norm with the PIC, it makes sense to make the output an 8-bit value, but where to stick the decimal point? Since the whole number portion of the answer is in the range 0-3, it makes sense to have 2 whole number of bits and 6 fractional bits. Hence, the decimal is after the second bits.

You may be concerned that the last entry in the square root table, 4.0, cannot be represented as a 2.6 format number. You re correct. However, representing the value 4.0 would require us to change the format to a 3.5 format for a single numerical value. If we choose instead to approximate the value 4.0 as the 2.6 format number 0b11111111 we would be awfully close to the actual value. Furthermore, allowing for this small error in a single representation gains us an extra decimal point for every output. The decision was made to accept this trade-off.
//----------------------------------------------
// Fnc	SQRT
// In	A 4-bit integer 
// Out	An approximate SQRT in 2.6 format
// Pur	This function computes a linearly
//	interpolated value for the SQRT
//	function.  There are some significant
//	data type issues that will have to
//	be resolved - note the use of "type"
//	in the function is a place-holder.
//----------------------------------------------
uint8_t SQRT(uint8_t x) {

    uint8_t lut[5] = {0x00, 0x80, 0xB5, 0xDE, 0xFF};
    uint8_t base, index, delta, frac;

    index = x >> 2;		
    delta = lut[index+1] - lut[index];
    frac = x & 0x03;	
    return(base + (frac*delta)>>2);
} // end SQRT

General theory

Look-up tables are wonderful creations; the same function is able to approximate any function just load up an array of sampled values into an array and use the same program structure. For the sake of the following discussion, call the input argument x. The operations performed in the look-up table function are dependent on: The image below shows the values of x in blue, the values stored in the LUT array in green and the indicies of those LUT entries in red. Some observations:


In order to perform the interpolation the bits of x are divided into two parts; those that are used to index the LUT array and those that are used in the interpolation. Let's start with how to form the array index.

You should be able to note that you can derrive the array index by dividing x by 2N-M and discarding the remainder. For example, a x value in the range [2N-M … 2N-M+1) corresponds to array index 1. Let this x = 2N-M+y. Now, (2N-M+y)/2N-M = 1 with a remainder of y. So this idea checks out. Dividing x by 2N-M (and discarding the remainder) is equivlent to shifting x right N-M bits. Hence the line of code index = x >> (N-M); Let's now look at how to form the fractional component.

Let the value of x be associated with array index α The interval from f(2α)(inclusive) … f(2α+1) (exclusive) is associate with 2N-M values of x. The fraction in the interpolation measures how far through this interval the value of x is. In order to get into the interval we used the most significant M bits of x. The remaining least-significant N-M bits are how far we are through the inteval. Hence we have the line of code frac = x & 0b0001…1, where there are N-M 1's in the binary mask.

Test your understanding

You can find the solutions embedded in the "source code" for this web page by right mouse clicking on this web page and selecting "view source". The solutions are in HTML comments.