In this Arduino Project we will see how we can make this cool looking compass using an Arduino, a MEMS Magnetometer and the Processing IDE. Here’s a demonstration video of the compass:
Overview
All we need for this project is a MEMS Magnetometer, for measuring the earth magnetic field, an Arduino Board and some jumper wires. As an example I will use the HMC5883L, a 3 – Axis Magnetometer integrated in the GY–80 breakout board.
How the Compass Work
Arduino Part
First we need to get the data from the sensor using the Arduino Board via the I2C protocol. Then using the X – Axis and Y – Axis values of the sensor we will calculate the heading and send its value to the Processing IDE via the Serial Port. The following code will do that job:
/* Arduino Compass
*
* by Dejan Nedelkovski,
* www.HowToMechatronics.com
*
*/
#include <Wire.h> //I2C Arduino Library
#define Magnetometer_mX0 0x03
#define Magnetometer_mX1 0x04
#define Magnetometer_mZ0 0x05
#define Magnetometer_mZ1 0x06
#define Magnetometer_mY0 0x07
#define Magnetometer_mY1 0x08
int mX0, mX1, mX_out;
int mY0, mY1, mY_out;
int mZ0, mZ1, mZ_out;
float heading, headingDegrees, headingFiltered, declination;
float Xm,Ym,Zm;
#define Magnetometer 0x1E //I2C 7bit address of HMC5883
void setup(){
//Initialize Serial and I2C communications
Serial.begin(115200);
Wire.begin();
delay(100);
Wire.beginTransmission(Magnetometer);
Wire.write(0x02); // Select mode register
Wire.write(0x00); // Continuous measurement mode
Wire.endTransmission();
}
void loop(){
//---- X-Axis
Wire.beginTransmission(Magnetometer); // transmit to device
Wire.write(Magnetometer_mX1);
Wire.endTransmission();
Wire.requestFrom(Magnetometer,1);
if(Wire.available()<=1)
{
mX0 = Wire.read();
}
Wire.beginTransmission(Magnetometer); // transmit to device
Wire.write(Magnetometer_mX0);
Wire.endTransmission();
Wire.requestFrom(Magnetometer,1);
if(Wire.available()<=1)
{
mX1 = Wire.read();
}
//---- Y-Axis
Wire.beginTransmission(Magnetometer); // transmit to device
Wire.write(Magnetometer_mY1);
Wire.endTransmission();
Wire.requestFrom(Magnetometer,1);
if(Wire.available()<=1)
{
mY0 = Wire.read();
}
Wire.beginTransmission(Magnetometer); // transmit to device
Wire.write(Magnetometer_mY0);
Wire.endTransmission();
Wire.requestFrom(Magnetometer,1);
if(Wire.available()<=1)
{
mY1 = Wire.read();
}
//---- Z-Axis
Wire.beginTransmission(Magnetometer); // transmit to device
Wire.write(Magnetometer_mZ1);
Wire.endTransmission();
Wire.requestFrom(Magnetometer,1);
if(Wire.available()<=1)
{
mZ0 = Wire.read();
}
Wire.beginTransmission(Magnetometer); // transmit to device
Wire.write(Magnetometer_mZ0);
Wire.endTransmission();
Wire.requestFrom(Magnetometer,1);
if(Wire.available()<=1)
{
mZ1 = Wire.read();
}
//---- X-Axis
mX1=mX1<<8;
mX_out =mX0+mX1; // Raw data
// From the datasheet: 0.92 mG/digit
Xm = mX_out*0.00092; // Gauss unit
//* Earth magnetic field ranges from 0.25 to 0.65 Gauss, so these are the values that we need to get approximately.
//---- Y-Axis
mY1=mY1<<8;
mY_out =mY0+mY1;
Ym = mY_out*0.00092;
//---- Z-Axis
mZ1=mZ1<<8;
mZ_out =mZ0+mZ1;
Zm = mZ_out*0.00092;
// ==============================
//Calculating Heading
heading = atan2(Ym, Xm);
// Correcting the heading with the declination angle depending on your location
// You can find your declination angle at: https://www.ngdc.noaa.gov/geomag-web/
// At my location it's 4.2 degrees => 0.073 rad
declination = 0.073;
heading += declination;
// Correcting when signs are reveresed
if(heading <0) heading += 2*PI;
// Correcting due to the addition of the declination angle
if(heading > 2*PI)heading -= 2*PI;
headingDegrees = heading * 180/PI; // The heading in Degrees unit
// Smoothing the output angle / Low pass filter
headingFiltered = headingFiltered*0.85 + headingDegrees*0.15;
//Sending the heading value through the Serial Port to Processing IDE
Serial.println(headingFiltered);
delay(50);
}
Code language: Arduino (arduino)
If you need more details how the MEMS Magnetometer work and how to get the data from it, you can check my MEMS Sensors Tutorial.
Processing IDE Part
Here first we need to receive the heading values coming from the Serial Port. For more details how is this done you can check my Arduino and Processing Tutorial.
The compass is actually a image, or more precisely, it’s composed of multiple transparent images loaded into Processing IDE. The images have the be located in the working directory of the sketch. After defining the images objects in the draw() section using the image() function we load the background image (which is optional, you can use just a simple color for the background). Then the Compass image is loaded which using the rotateZ() function is rotated with the values of the heading. At top of them the Compass Arrow image is loaded.
Here’s the Processing IDE Code:
/* Arduino Compass
*
* by Dejan Nedelkovski,
* www.HowToMechatronics.com
*
*/
import processing.serial.*;
import java.awt.event.KeyEvent;
import java.io.IOException;
Serial myPort;
PImage imgCompass;
PImage imgCompassArrow;
PImage background;
String data="";
float heading;
void setup() {
size (1920, 1080, P3D);
smooth();
imgCompass = loadImage("Compass.png");
imgCompassArrow = loadImage("CompassArrow.png");
background = loadImage("Background.png");
myPort = new Serial(this, "COM4", 115200); // starts the serial communication
myPort.bufferUntil('\n');
}
void draw() {
image(background,0, 0); // Loads the Background image
pushMatrix();
translate(width/2, height/2, 0); // Translates the coordinate system into the center of the screen, so that the rotation happen right in the center
rotateZ(radians(-heading)); // Rotates the Compass around Z - Axis
image(imgCompass, -960, -540); // Loads the Compass image and as the coordinate system is relocated we need need to set the image at -960x, -540y (half the screen size)
popMatrix(); // Brings coordinate system is back to the original position 0,0,0
image(imgCompassArrow,0, 0); // Loads the CompassArrow image which is not affected by the rotateZ() function because of the popMatrix() function
textSize(30);
text("Heading: " + heading,40,40); // Prints the value of the heading on the screen
delay(40);
}
// starts reading data from the Serial Port
void serialEvent (Serial myPort) {
data = myPort.readStringUntil('\n');// reads the data from the Serial Port and puts it into the String variable "data".
heading = float(data); // Convering the the String value into Float value
}
Code language: Arduino (arduino)
Here you can download the files from project, the images and the source codes:
Does this compensate for tilt?
No, tilt compensating is not implemented in this code.
Hi, Great website.
Could you please explain more about converting the sensors output to angles and the orientation .
I am looking for the angle and orientation of sensor at every point I move the sensors to known distance.
I actually want the data to be stored or seen in a data logger like the angle and orientation .
I am new to Arduino sorry if the question is so simple and dumb.
Thanks
Hi friend! Great design!
I would like to do this on a TFT LCD screen 3.2 “is possible?
Thank you
Beautiful design but …
How to use it on a TFT LCD screen 3.2″, for example, instead of the computer screen? Thanks
Thanks. Well check my Arduino TFT Touch Screen Tutorial for that.
hi dejan, thanks a lot for all your great projects.
i tried this one but i get a null pointer exception at line 29.
do you know what is wrong?
What is line 29? Something the Processing IDE code might get stacked for some reason and you need to re-plug the Arduino to your USB port.
hi dejan,
sorry i was not clear. the processing sketch fails at line 29; there is this “null pointer exception”. i tried to replug the arduino but still it bugs.
this is surprising because your codes for the mems sensors all work well on my system, and your radar project also works like a charm.
sorry i just looked at the details of the fail. the files compass.png, compassarrow.png and background arrow.png are missing. how can i get them?
Hi Dejan, i should have looked more carefully at your page, found the picture files and once included the system works as described.
thanks a lot!!
Nice to hear it works! 🙂
This code will fail on Teensy3, Arduino Zero, Arduino Due, ChipKit, ESP8266 and all other newer 32 bit boards.
The problem is incorrect conversion from two 8 bit unsigned numbers to a signed integer. The math actually computes a number from 0 to 65535, because the numbers from Wire.read() are 0 to 255. On 8 bit AVR, the incorrect computation magically becomes -32768 to +32767 when stored into a 16 bit “int”. But on 32 bit chips, the computation results in an unsigned 0 to 65536, because “int” is 32 bit.
The “int” variables need to be changed to “int16_t” to make this work on any 32 bit board.
Thanks, that’s a good remark!