Apr 29

Shoehorn a accelerometer heading calculation into an ATTiny 261

working on a recent project, I wanted to get an ATTiny261 to calculate a heading in degrees from an accelerometer. The accelerometer (an Xtrinsic MMA8453Q 3-Axis, 10-bit/8-bit Digital Accelerometer) has an I2C interface with digital X/Y/Z readings, so no analog to digital mess, just simple math, right? RIGHT….

Given all the UAV stuff out there, finding simple heading orientation calculations based on accelerometer data is pretty quick. Basically insert the Accelerometer info in as ax / ay / az into:

xAngle = 180.0 * atan( ax / (sqrt(square(ay) + square(az)))) / 3.14259;
yAngle = 180.0 * atan( ay / (sqrt(square(ax) + square(az)))) / 3.14259;
zAngle = 180.0 * atan( sqrt(square(ax) + square(ay)) / az) / 3.14259;

For my purpose, I only needed the yAngle. Seemed simple enough since I was using AVR GCC, to read the data from the accelerometer (left as an exercise to the reader) and insert the numbers into the formula, compile and poof, done!

Well, it compiled, but I quickly realized when data memory turned up over 400% full and program memory was over 600% full that taking the simple approach with the little ATTiny 261 wasn’t going to work.

Studying the code, several problems turn up. the AVR GCC implementation of atan is gigantic. that’s the issue w/ the data space. Doing floating point is also probably contributing to code bloat. So had the following requirements:

  1. a trimmed down atan function
  2. a trimmed down square root
  3. scaled integer math

Attach the atan first, that’s the worst offender. Obviously need some type of table lookup with interpolation between the table points. Found an algorithm for Excel that seemed easy to re-purpose. To calculate a point in-between two table points it simply draws a line between the two points and uses the intersection as the returned value. Usable for any number of points depending on how accurate of a curve you want.

Shove this into an Excel Macro:

Public Function Linterp(Tbl As Range, x As Double) As Variant
‘ linear interpolator / extrapolator
‘ Tbl is a two-column range containing known x, known y, sorted x ascending

Dim nRow As Long
Dim iLo As Long, iHi As Long

nRow = Tbl.Rows.Count
If nRow < 2 Or Tbl.Columns.Count <> 2 Then
Linterp = CVErr(xlErrValue)
Exit Function ‘——————————————————–>
End If

If x < Tbl(1, 1) Then ' x < xmin, extrapolate from first two entries iLo = 1 iHi = 2 ElseIf x > Tbl(nRow, 1) Then ‘ x > xmax, extrapolate from last two entries
iLo = nRow – 1
iHi = nRow
Else
iLo = Application.Match(x, Application.Index(Tbl, 0, 1), 1)
If Tbl(iLo, 1) = x Then ‘ x is exact from table
Linterp = Tbl(iLo, 2)
Exit Function ‘—————————————————->
Else ‘ x is between tabulated values, interpolate
iHi = iLo + 1
End If
End If

Linterp = Tbl(iLo, 2) + (Tbl(iHi, 2) – Tbl(iLo, 2)) _
* (x – Tbl(iLo, 1)) / (Tbl(iHi, 1) – Tbl(iLo, 1))

End Function

You need two tables in Excel. One is your atan table. column 1 (E in my example) is your X values and column 2 (F in my example) is the atan value for the corresponding X. Make as many rows as you need to get high enough accuracy in your atan table. I ended up with 33.

2nd table in Excel is to evaluate the resulting accuracy of your atan function. This table has 4 columns. Column 1 (A in my example) contains the range of X values you expect to generate. Since I had my eye on integer math, I assumed -128 to +128. Column 2 (B) simply contains the actual atan value. So my B2 cell contained =ATAN(A2)*180/3.14159 yeah, note I shoved in conversion from radians to degrees. No sense including code in the ATTiny to do the *180/3.14159, simply hard-code it into the atan table results. Save a few CPU cycles later. Now Column 3 (C) is the magic. This calls the Excel macro, so C2 contains =Linterp($E$1:$F$33, A2) note that the first parameter there is the range of our first table, the atan table. Finally, I stuck in column 4 (D) containing =(C2-B2)/C2, ie the percentage deviation. It may also be instructive to graph columns B and C (the actual and interpolated values)

Now the manual part. play with your atan table, both the spacing of the points and the number of points until you get what you feel is a reasonable fit without excessive points. Remember that ATTiny261 data fill of 400%? that’s the GCC table interpolation for ATAN, way more accurate than I needed… I got “good enough” with only 33 points. Of course I let the error creep up to almost 1% at times, but that’s the price you pay for saving memory.

Here’s my resulting Atan Table….

Trimming down the Square root function actually became really easy when I stumbled across the fact that sqrt(square(ax) + square(az)) can be approximated by MAX( ABS(ax),ABS(ay)) + Min(ABS(ax),ABS(ay))/2 . Not only does the square root go away, but the two multiplies go away to be replaced by a divide by 2. Sweet!

Finally converting to integer math wasn’t bad. Simply used 16 bit numbers with a multiplier of 128. That left a result that actually fit in the ATTiny 261’s miniscule memory.

Or so I thought, that is until I added one more routine to my code. Everything went wonky at that point and the code stopped working. What gives?!? Ultimately tracked it down to the fact that even at only 33 entries, my atan table was consuming almost all the meager 128 bytes of SRAM, leaving almost no stack space. Adding that one more routine caused stack overflow 🙁

Thus the final trick to squeezing all this into an ATTiny 261. Move the atan table to program memory! Easy enough to now define my atan table like this:

const int16_t TanTable[] PROGMEM =
{-16384, -2944, -1280, -704, -436, -295, -205, -154, -116, -90, -77, -64, -52,
-39, -26, -13, 0, 12, 25, 38, 51, 64, 76, 89, 115, 153,
204, 294, 435, 704, 1280, 2944, 16384 };

and then you have to change your array access to look like this:

(int16_t)pgm_read_word(&(TanTable[iLo]))

But with those changes, I was finally able to shoehorn in the heading calculation into the ATTiny261!

Leave a Reply

Your email address will not be published.