Arduino, HMC5883L, and a Voltmeter: Voltmeter Compass

I recently purchased an AdaFruit HMC5883L Compass Breakout board for $10:

It’s a simple device. It will work with 3.3V and 5V, and  uses I2C for communications. Interfacing with an Arduino is very easy.

I have a project in the back of my mind to use one of these to indicate if I’m straying off a set course. I was thinking of using a fancy LCD display but they are kind of expensive and use a lot of power if run constantly.

Then it occurred to me to just use a voltmeter. They are much less expensive and have a nice retro feel. I’ve seen volt meters used in the past with pulse-width modulation (PWM) to allow very precise positioning of the needle.

I have a couple of 5V volt meters I picked up somewhere cheap in the past couple of years. I could have sworn I got them from All Electronics, but I don’t see them there now.

First, I used the stock Arduino Fade sketch (Examples | Basics | Fade). This uses PWM to control an LED’s brightness. Since this sketch simply varies the output pins voltage from 0V to 5V, I could just connect the meter to the output pin rather than an LED. Sure enough, it worked and the meter’s needle would swing back and forth between 0V and 5V.

Next I tested AdaFruit’s demo sketch, found at Examples | AdaFruit_HMC5883_Unified | MagSensor (once you install the library). This, too, worked perfect and I was getting good headings from the magnetic sensor.

Note: There is a spot in the sketch to put in the correction for magnetic declination. This corrects the difference between true north and magnetic north for your location. There is a 15 degree difference here, so it can make a difference.

With the basics ironed out, I put together the hardware:

hmc5883L-fig1

I could re-use quite a bit of the example sketches’ source code. I just need to write a bit of code to detect the difference between the desired course and the actual heading, and compute a correction angle.

Then, the function to drive the meter simply uses this angle to position the needle. For my meter, the limits (0V and 5V) are roughly 45 degrees from the center. If the correction angle is > 45 degrees, I set it to 45, then use the map function to map the correction angle into 0-255 for the meter.

The whole thing is really quite simple, it turns out. I spent the most time trying to write a correction angle algorithm that didn’t need an if statement, but I never succeed. Drats.

Here is a short video clip of the compass in action.

and here is the code I used:

#include                <Adafruit_HMC5883_U.h>
#include                <Adafruit_Sensor.h>
#include                <Arduino.h>
#include                <Wire.h>

// ----------------------------------------------------------------------------
// Constants
// ----------------------------------------------------------------------------

const int               meterPin        = 3;                // must be on a PWM pin

// ----------------------------------------------------------------------------
// global variables
// ----------------------------------------------------------------------------

/* Assign a unique ID to this sensor at the same time */

Adafruit_HMC5883_Unified
                        mag             = Adafruit_HMC5883_Unified(12345);

// ----------------------------------------------------------------------------
// Forward declarations
// ----------------------------------------------------------------------------

void displaySensorDetails(
    void
    );
int getCorrection(
    int                 course,
    int                 heading
    );
int getHeading(
    );
void meter(
    int                 correction
    );
int my_getc(
    FILE                *t
    );
int my_putc(
    char                c,
    FILE                *t
    );

// ----------------------------------------------------------------------------
// Display info about the sensor

void displaySensorDetails(
    void
    ) {

sensor_t sensor;
mag.getSensor(&sensor);

Serial.println("------------------------------------");
Serial.print  ("Sensor:       "); Serial.println(sensor.name);
Serial.print  ("Driver Ver:   "); Serial.println(sensor.version);
Serial.print  ("Unique ID:    "); Serial.println(sensor.sensor_id);
Serial.print  ("Max Value:    "); Serial.print(sensor.max_value); Serial.println(" uT");
Serial.print  ("Min Value:    "); Serial.print(sensor.min_value); Serial.println(" uT");
Serial.print  ("Resolution:   "); Serial.print(sensor.resolution); Serial.println(" uT");
Serial.println("------------------------------------");
Serial.println("");

delay(500);

} // displaySensorDetails

// ----------------------------------------------------------------------------
// determine the angle (in degrees) of the correction

int getCorrection(
    int                 course,
    int                 heading
    ) {

//printf("@getCorrection: course: %d; heading:%d; ", course, heading);

int                     normalizedHeading;                  // heading adjusted for a 0 course
int                     correctionAngle;                    // amount to turn and the direction (< 0 means turn left)

if (course <= 180)
    normalizedHeading = (round(heading) + (0-course) + 360) % 360;
else
    normalizedHeading = (round(heading) + (360-course) + 360) % 360;
if (normalizedHeading <= 180)
    correctionAngle = -normalizedHeading;
else
    correctionAngle = (360 - normalizedHeading) % 360;

//printf("  correctionAngle: %d\n", correctionAngle);

return correctionAngle;

} // getCorrection - the gist of this code is from the Adafruit HMC5883L example

// ----------------------------------------------------------------------------
// get current heading

int getHeading(
    ) {

float                   declinationAngle;
float                   heading;
int                     headingDegrees;

sensors_event_t event;
mag.getEvent(&event);

heading = atan2(event.magnetic.y, event.magnetic.x);

declinationAngle    = 0.26; // Fixed for my town
heading             = heading + declinationAngle;

// Correct for when signs are reversed.
if(heading < 0)
    heading = heading + 2*PI;

// Check for wrap due to addition of declination.
if(heading > 2*PI)
    heading = heading - 2*PI;

// Convert radians to degrees for readability.
headingDegrees = round(heading * 180/M_PI);

return headingDegrees;

} // getHeading

// ----------------------------------------------------------------------------
// make the meter indicate the current correction

void meter(
    int                 correction
    ) {

int                     maxCorrection;

if (correction < 0)
    maxCorrection = max(-45, correction);
else
    maxCorrection = min(45,  correction);

analogWrite(meterPin, map(maxCorrection, -45, 45, 0, 255));

} // meter

// ----------------------------------------------------------------------------
// needed by fdevopen

int my_getc(
    FILE                *t
    ) {

char                    c;

while (Serial.available() == 0) {}
c = Serial.read();
Serial.write(c);                                            // echo

if (c == '\r') {
    c = '\n';
    Serial.write(c);
    }

return c;

} // my_getc

// ----------------------------------------------------------------------------
// needed by fdevopen

int my_putc(
    char                c,
    FILE                *t
    ) {

if (c == '\n')
    Serial.write('\r');

Serial.write(c);

} // my_putc

// ----------------------------------------------------------------------------

void setup(void)
{
Serial.begin(9600);
fdevopen(&my_putc, &my_getc);                               // open output so printf works
Serial.println("HMC5883 Magnetometer Test"); Serial.println("");

pinMode(meterPin, OUTPUT);

// Initialise the sensor
if(!mag.begin()) {
    Serial.println("Ooops, no HMC5883 detected ... Check your wiring!");
    while(true) {}
    }

// Display some basic information on this sensor
  displaySensorDetails();
}

// ----------------------------------------------------------------------------

void loop(
    void
    ) {

int                     correction;
int                     course          = 0;                // true direction we want to go
int                     heading;

heading    = getHeading();
correction = getCorrection(course, round(heading));
meter(correction);

//printf("Heading: %4d     Course: %4d    Correction: %4d\n",
//  heading, course, correction);

delay(100UL);

}
This entry was posted in c-arduino and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s