Embedded Trig
Here's some algorithms to make it easier to compute complex trig
functions.
Published in Embedded Systems Programming, May 1991
 |
For hints, tricks and ideas about better ways to build embedded systems, subscribe to The Embedded Muse, a free biweekly e-newsletter. No hype, just down to earth embedded talk. 23,000 other engineers subscribe. It takes just a few seconds (all we need is your email address, which is shared with absolutely no one) to subscribe to the Embedded Muse. |
The real world is an ugly place where the laws of physics reign
supreme. Whether an input device is a sensor measuring some physical
parameter, or our controller causes some event to happen, frequently
we model some aspect of the real world in our code.
This means sooner or later we have to dirty our hands with math.
Sometimes I wonder how pure computer science types cope with coding
Fast Fourier Transforms or regression equations with perhaps little
calculus background, but perhaps that is a philosophical subject
better suited for a late night party discussion...
Recently I talked at length with an engineer writing code to control
a milling machine's X-Y table. He was looking for a quick assembly
language Sine routine, since the motion of the mill's table is
related to the sine of the angle traversed by the controller's
stepper motors.
An awful lot of embedded systems compute trig functions to sequence
an output or translate incoming data. Page though any physics
book; sines and cosines seem to describe just about every sort
of phenomena. Vectors, kinematics, rotation - every sort of two
or three dimensional motion is best described by a trig relationship.
The X-Y Digital to Analog converters in a vector CRT are synchronized
in a trig ballet, as is the complex motion of a robot arm or a
flatbed plotter.
Working in C with lots of free memory we never have to worry about
taking the sine or cosine of an angle. The compiler's libraries
have trig resources; we just invoke the routines without giving
them much thought. If only all of the embedded world were so easy!
Too many of us fight for every last byte or microsecond. Maybe
we can't afford the overhead of a standard math library. Perhaps
we're working in assembly where such resources just don't exist.
Packaged libraries are no panacea. The vendor selects some speed
versus precision mix to satisfy 90% of the customers, giving a
mix that might not be right for your particular application. Worse,
(gasp!) the libraries that come with some well-known C compilers
are not reentrant, so you can't use these functions in interrupt
or multitasking routines. Even in C sometimes ROM space is a problem;
if you only need a cosine routine, why link in an entire trig
module?
One solution is to roll your own trig algorithms. Very high precision
trig routines, in particular, are easy to write.
Errors
Trigonometric functions yield irrational results. That is, they
have no exact solution except at certain "round" numbers
like pi/2. However, given a decent algorithm and enough computer
time any desired level of precision is possible.
Just how accurate is a particular algorithm? Most accuracy measurements
(or better, precision: accuracy is really a measure of repeatability,
but in most computer literature the two words are used interchangeably)
use one of two methods to answer this question.
Absolute accuracy tells us how much an approximation is in error
at any point. Mathematically it is:
|f(x)-a(x)|
In English, this is the absolute value of the correct answer (f(x))
minus the one given by the approximation (a(x)). In one sense
this is just what we're looking for. However, what if the function's
value is very close to 0? An absolute accuracy of .00001 sounds
great, but it is a 50% error if f(x) is .00002.
Relative accuracy is normalized to f(x) to account for points
around any singularity. It is expressed as:
|f(x)-a(x)| / |f(x)|
and closely resembles percentage error. A relative accuracy of
.01 is only an absolute error of .001 if f(x) is 10, but is .0002
if the function's value is .02.
Select an algorithm based on its relative accuracy if your code
requires extremely precise answers over a wide range of inputs.
Be aware that most functions behave oddly near certain points.
The sine, for example, changes very slowly in the vicinity of
0, but accelerates rapidly near pi/4.
Don't be lulled into a quest for the best possible accuracy. High
precision carries a correspondingly high computational cost. Know
what the project needs, and then select the appropriate algorithm.
No algorithm will behave as expected if the floating point package
(add, subtract, divide and multiply) loses much precision. Every
addition, multiplication, division and subtraction in even the
best package will reduce the answer's precision by some small
amount. Never make assumptions about your floating point libraries,
whether written by your company, supplied by a compiler vendor,
or implemented in a coprocessor chip.
Trig Basics
Most programmers think in terms of degrees, with 360 forming a
circle. The radian is a more natural unit of measurement for angles.
Radians are based on pi, the number that relates diameter to circumference
and ties all of trigonometry together.
A circle consists of 2 * pi radians. Thus, pi radians corresponds
to 180 degrees, pi/2 to 90 degrees, etc. The formal relationship
between degrees and radians is:
2 * pi radians = 360 degrees
so,
radians = (pi/180) * degrees
To put this in easier-to-understand terms, one radian is about
57 degrees.
The trig libraries used by different languages seem to almost
arbitrarily support radians or degrees. The following discussion
is in radians.
The standard trig functions are interrelated as follows:
sin(x + 90)=cos(x) (in degrees)
sin(x + pi/2)=cos(x) (in radians)
tan(x)=sin(x)/cos(x)
There's little reason to develop approximations for both the sine
and cosine, since it is so easy to convert between them. Use these
relations, write one approximation function, and save coding time
and memory.
Floating Point Approximations
We really don't have deterministic ways to compute irrational
functions. There is no magic formula that figures a sine or cosine.
In the computer biz we rely on an amazing fallout of calculus:
we can approximate any continuous function, over some range, using
nothing more exotic than polynomials.
This is truly profound. Does your instrument process some obscure
input whose behavior is a really weird curve? If it is continuous,
then it is possible to come up with one or more polynomials that
describe it any desired degree of accuracy.
One way to figure a sine is the Taylor series:
sine(x)=x-(x**3)/3! + (x**5)/5! - (x**7)/7! + . . .
We all talk about Taylor series approximations, but in fact no
one uses them. The Taylor series converge too slowly to be useful.
In practice we use approximations derived from Chebyshev series
and other expansions that have been extensively modified for quick
convergence. (The bible of approximation is John Hart's "Computer
Approximations", published in 1968 by John Wiley & Sons.)
Figure 1 shows a C function that computes the cosine of an angle
x in radians. The accuracy is measured in absolute terms and slightly
exceeds 9.6 decimal digits.
Variable quad assumes the values 0, 1, 2, or 3, corresponding
to the quadrant x lies in. Quadrant 0 is corresponds to angles
of 0 to 90 degrees, quadrant 2 to 90 to 180 degrees, three to
180 to 270 degrees, and the fourth quadrant is the last 90 degrees
of a circle. The routine decomposes the input angle (x) into quad
and frac, where frac lies between 0 and pi/2, the range of the
polynomial.
The coefficients p0 to p5 form an equation of the form: cos(x)=p(x**2).
In other words, the equation is:
p0 + (p1*x**2) + (p2*x**4) + (p3*x**6) + (p4*x**8) + (p5*x**10)
Different systems require a wide range of accuracy, from low precision
fast approximations for real time imaging to slow but ultra-precise
numbers for orbit corrections. Following are a number of coefficient
sets with different speed/accuracy tradeoffs. All coefficients
cover the same range as the function shown; they plug directly
into the code in the Figure. Of course, where a coefficient is
zero, remove that entire term from the polynomial to save two
floating point operations.
Absolute accuracy: 3.22 decimal digits
p0= 0.99940307
p1= -0.49558072
p2= 0.03679168
p3= 0.0
p4= 0.0
p5= 0.0
Absolute accuracy: 12.12 decimal digits
p0= 0.99999999999925182
p1= -0.49999999997024012
p2= 0.04166666647338454
p3= -0.00138888841800042
p4= 0.000024801040648456
p5= -0.000000275246963843
p6= 0.000000001990785685
Integer Approximations
Embedded systems are peculiar beasts. Some applications demand
very fast results with practically no accuracy. Sometimes we have
no floating point capability at all, and just need a rough integer
result. If the D/A converters on your vector CRT are only 8 bits,
a floating point solution will be too accurate and too slow.
Digging through my algorithm collection I found two separate integer
approximations based on the set of coefficients (which as used
as shown in Figure 1):
p0= 0.99940
p1=-0.49558
p2= 0.03679
The algorithms scale these numbers to integer approximations by
multiplying by 255 or 65536. Then they solve for an angle that
ranges from 0 to 255 or 65536 as follows:
sine(x)= .99940*255 - 0.49448*255*x**2 + .03679*255*x**4
or,
sine(x)= 255 - 126*x**2 + 9*x**4
Theoretically this is a nice, fast way to compute a low precision
integer sine. It doesn't really work well unless you use 32 bit
math, since the x**2 and x**4 terms quickly grow to huge numbers
we can't stuff into a byte or word.
A crude alternative is to use a table lookup for the trig values.
This might eat up a lot of memory, even if you only need a byte's
worth of accuracy. It is, however, the fastest method you'll find.
Where memory is a problem limit the table's size and then interpolate
between values. Suppose you need 16 bit accuracy (i.e., one part
in 65536, or .002%). A table of 65000 integers will fill several
PROMs, and just is not practical.
It might be better to build a table of 256 integers that approximates
the sine every 256 points. That is, the table has an entry for
the sine at scaled input values of 0, 256, 512, ... 65536.
Now write a routine to decompose the input value x. Compute an
N and D such that x=256*N+D. Then, compute the sine by:
sine(x)=f(N) + (D/256)*(f(N+1) - f(N))
(where f(a) is the table lookup at point a).
Suppose the input argument is 300. N is 1, D is 44. The formula
just sums the sine at point 256 (the second table entry) with
the difference between that point and the next from the table,
divided by the ratio of the input value to the difference in table
entries.
This is an example of a linear interpolation. Whenever an input
value corresponds exactly to a table entry we simply return that
entry. Where the input falls between two values in the table,
we draw a straight line connecting the two points and return an
answer from that line. Of course, the trig functions are not lines,
so we do loose some precision doing this. You can increase accuracy
by making the table bigger, or coming up with a better, non-linear
interpolation. As with all approximations, this involves some
tradeoff between speed, memory, and accuracy.
Conclusion
Computer approximations receive little attention, yet are used
in most systems. I find them fun to experiment with; so many sorts
of formulas and algorithms yield wildly different accuracies and
efficiencies. Over the years I've collected a huge folder of techniques
that range from the ridiculous to the truly elegant, but no one
method is ideal for all applications.
*********************************************************
double cosine(double x)
{
double p0,p1,p2,p3,p4,p5,y,t,absx,frac,quad,pi2;
p0= 0.999999999781;
p1=-0.499999993585;
p2= 0.041666636258;
p3=-0.0013888361399;
p4= 0.00002476016134;
p5=-0.00000026051495;
pi2=1.570796326794896; /* pi/2 */
absx=x;
if (x<0) absx=-absx; /* absolute value of input */
quad=(int) (absx/pi2); /* quadrant (0 to 3) */
frac= (absx/pi2) - quad; /* fractional part of input */
if(quad==0) t=frac * pi2;
if(quad==1) t=(1-frac) * pi2;
if(quad==2) t=frac * pi2;
if(quad==3) t=(frac-1) * pi2;
t=t * t;
y=p0 + (p1*t) + (p2*t*t) + (p3*t*t*t) + (p4*t*t*t*t) + (p5*t*t*t*t*t);
if(quad==2 | quad==1) y=-y; /* correct sign */
return(y);
};
Figure 1: Function to compute cos(x)
*********************************************************
|