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:
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); }