In previous posts, I talked about a method of transforming magnetometer readings to compass headings, then experimented with using those transformations on real (but static) data. In this post, I'll present a working prototype of a vehicle compass using the methods I discussed earlier.
Read on for more details.

Update: This article is part of a larger series on building an AVR-based compass. You may be interested in the other articles in the series.

Goals

My goals for the prototype were:

  • Create a working compass that reads out human-readable heading information in near-real-time.
  • Achieve accuracy within a few degrees regardless of sensor orientation.
  • Use a platform with good debugging and development tools, and a short edit-compile-test cycle.
  • Create a code base that does things in the most obvious and easy-to-understand way.
  • Avoid storing more than a few points in memory at any one time.

I consciously decided to ignore the following for the initial implementation:

  • Speed
  • Code size
  • Slick user interface
  • Finding optimal values for any tunable parameters used

Basically, I was trying to prove that the method I had in mind was practical and worth working on further. Assuming it was, I wanted to have a good starting point for experimentation, optimization and porting to a microcontroller platform.

The Code

My working prototype code is available for download: compass-tst2.tar.gz (14KB gzipped tarball)

Hardware Setup

For testing, I used the same hardware as I previously used for gathering my initial static data set: A Raspberry Pi with a wireless network card and battery power, connected to an HMC5883L magnetometer via an I²C bus. All of the components were placed on a ball-bearing turntable (of the cheap plastic variety) for ease of testing.

This allowed me to edit, compile and debug from my desktop computer, while still being able to test changes immediately.

Results

After finding and fixing a number of bugs, and refining the calibration process a bit, I was able to get surprisingly accurate headings (within a few tenths of a degree in most cases).

I experimented with a variety of sensor orientations, and also with placing hard- and soft-iron interferers on the platform with the sensor. Except in cases where hard-iron interference saturated the sensor, I was still able to get good heading information.

This approach to calibration and interpretation of readings clearly works well enough to implement an eight-direction vehicle compass. In fact, it's likely horrible overkill...

Debugging

I made a few careless errors in the first draft of the code. At the point where I was translating point 'W' to use a coordinate system with point 'C' at the origin, I subtracted the Z coordinate of 'C' from the Y (not Z) coordinate of 'W'.

My first attempt also had the matrix composition in the wrong order.

Both of these were reasonably easy to solve by printing out the coordinates at various points and checking the results for sanity. (I have left a lot of this intermediate output in the example code, as I expect it will come in handy as I optimize and port.)

Once things were more-or-less working, I found that the calibration process was extremely sensitive to speed. Unless the platform was rotated at a uniform speed, the center point 'C' would end up a significant distance from the correct position, and accuracy would suffer. To alleviate this problem, I changed the calculation of C to consider only points a certain distance apart (rather than every point sampled). This made the calibration process largely insensitive to rotation speed over a wide range.

The condition for detecting termination of the calibration loop took some tweaking, and still isn't completely reliable. (It is possible -- though unlikely -- to rotate through the starting point and still have no sample close enough to point 'N' to trigger the end-of-calibration condition.)

Next Steps

Additional testing is needed. I should check the headings returned against an accurate angle reference. It would be a good idea to test the device in a real car or truck. Finally, I should test that calibration data remain valid across a wide temperature range.

There are lots of "magic numbers" (tunable parameters) in calibrate.c:get_nwc(), and at least one (gain) in the sensor initialization. Some effort in finding ideal values for these would produce better results with no additional cost in code complexity.

The three functions for finding the rotation matrices (rot_find_R*()) are nearly identical and can be combined with a little bit of cleverness.

There's no reason to store the Rx, Ry and Rz matrices individually; we should be able to compute each rotation and compose it with the previous steps before going on.

We don't actually need to compute the angle of rotation \theta for each step. We only actually care about \sin\theta and \cos\theta. Since \theta=\tan^{-1}\frac{a}{b} we know that:

\sin\theta=\frac{a}{\sqrt{a^2+b^2}}

and

\cos\theta=\frac{b}{\sqrt{a^2+b^2}}

It's possible that we could save some code space by avoiding having to calculate sines and cosines, at the cost of having to calculate square roots. (We still need arctangents in any event.)

The jitter calculation is probably unnecessary  since the result is mostly due to a property of the sensor which (we will act like we're entitled to assume) is invariant.

Using fixed-point math instead of floating point is a big step, but one that might result in major space savings while keeping "good enough" accuracy.