*******
Please visit https://proteshea.com/3-axis-magnetometer-with-arduino-uno/ for a complete list of materials for this project.
*******
IntroductionIn this project, we’ll be interfacing the HMC5883L 3-axis magnetometer to an Arduino Uno. This device can measure the magnitude and direction of the Earth’s magnetic field. It’s a low-power device and can be found in mobile phones or navigation systems to provide an accurate compass heading. You can also use them to detect ferrous (contains iron) metals since the iron within the metal changes the magnetic field when it’s in close proximity to the sensor.
Don’t get lost out there, let’s get started!
20x4 Character LCDThe 20x4 LCD adds two extra rows and four extra columns per row compared to the 16x2 LCD. Similar to the 16x2 that we've used in previous projects, the 20x4 LCD uses the Hitachi controller so the commands and interfaces are the same. It also has the same 16-pin header, allowing you to unplug the 16x2 LCD and plug in the 20x4 without changing any wiring. The only thing we have to change is one line of code, lcd.begin(20, 4), which specifies the columns (first argument) and rows (second argument) of the LCD. An image of the 20x4 LCD is shown below.
The HMC5883L 3-axis magnetometer can accurately measure the magnitude and direction of the Earth’s magnetic field in the x, y, and z direction. As a result, it can be used to provide a compass heading which is why it is also referred to as a digital compass. It is a low-powered device in a small form factor, allowing you to embed it in just about any project that requires a compass heading. A table is provided below that gives some specifications about the module.
The breakout board from Although the datasheet for the HMC5883L IC consists of 5 pins: GND, VIN, DRDY, SCL, and SDA (image shown below). The GND and VIN pins are used to power the device. Although Parallax’s datasheet says that the module can operate from 2.7V to 6.5V, we had trouble getting the magnetometer to work at 5V and thus, we recommend using 3.3Vdc. The GND and VIN pins will connect to GND and 3.3V pins on the Uno, respectively.
To communicate with the device, we use the I2C protocol which only uses two pins, SCL and SDA. We use this to configure the registers on the device (i.e., setting the measurement mode and output rate), and acquire the X, Y, and Z magnetic field measurements. The device can only be 7-bit addressed at 0x1E, so you cannot have more than one of these devices on the I2C bus at one time. The SCL and SDA pins will connect to the Uno analog pins A5 and A4, respectively.
The DRDY (data ready) pin is used to tell the master device (Uno) that data is ready in the X, Y, and Z registers. The maximum output rate is 75Hz, but by using the DRDY pin, you can achieve up to 160Hz. You can connect this pin to an interrupt pin on the Uno for an efficient way to obtain the data. However, we will not be using this DRDY pin in this project.
This device has a magneto-resistive sensor on each of its 3 axes to measure the magnetic fields. In the presence of a magnetic field, the resistance of these elements changes which causes a change in voltage across the outputs. This change in voltage is measured on each axis by the device’s 12-bit ADC and then the measurement is written to the corresponding X, Y, and Z 8-bit data registers.
An address map of each register is shown in the table below. The registers highlighted in yellow indicate the register we will be reading from to obtain the measurements of the magnetic field on each axis. To learn more about the details of each register, please consult the datasheet for the HMC5883L IC.
As you rotate the device about the X, Y, and Z axes, the magnetic field of each axis will change, as shown in the image below. The device must be oriented so that the X-Y plane (top plane of the board) is parallel to the ground, and also pointing upwards.
You could mount the magnetometer to Adapticon to provide stabilization. You will need an M2 male-to-female hex standoff, M2 nut, and an M2 screw to mount it. This keeps the X-Y plane parallel to the ground and allows you to rotate the FuelCan about the Z-axis, so you can get accurate compass headings. Make sure to use non-ferrous mounting hardware to avoid interference with the magnetic field. An image of the magnetometer mounted to Adapticon is shown below.
We just left the device attached to the 12″ jumper wires so we can freely move it around in all directions.
If you are using a solderless breadboard, use the schematic below to make the necessary connections for the 16×2 LCD. The magnetometer connects directly to the Uno with 12″ F/M jumper wires. We are using the 3V3 supply from the Uno instead of the 3V3 rail of the FuelCan since the 0.1″ pitch of the male header pins is too close to mount the test-lead clip cables next to each other.
NOTE: The schematic shows a 16×2 character LCD, but the 20×4 LCD is pin compatible. The only change you have to make is in the software.
If you haven’t mounted the Uno onto the prototyping area of the FuelCan, go ahead and do that. If you are using a breadboard instead of Modulus, place the breadboard in the bottom storage compartment to limit the length of the jumper wires. You’ll need to supply +5V and GND to the power and ground rails on the breadboard by using the provided banana jack to test-lead clip cables. You will need two male header pins to mount the test-lead clips on the breadboard side. This is used to power the LCD.
Next, plug the Type A side of the USB cable into USB1 receptacle and the Type B side into the Uno’s receptacle. Plug in the 6′ Type A to Type A cable – one end into the external USB connector and the other end into the host (i.e., computer). Power up the FuelCan with the AC-DC power adapter.
For additional information about the Fuelcan-910, click here.
Software ExplanationWe communicate with the device via I2C, so be sure to include the Wire.h library. Before we can start acquiring data from the device, we first have to configure it using functions setOperatingMode() and setSamples() to set the operating mode to continuous and set the number of samples averaged per measurement output to 8. Everything else will be left as default.
Now that the registers are configured, we can start acquiring the raw X, Y, and Z data with the function getXYZ(). This function grabs each of the 16-bit data for X, Y, and Z. The Wire.requestFrom() function within getXYZ() is used to request the 6 bytes of data.
Once we have the raw X, Y, and Z values, the function convert() is called to convert the raw count to Gauss (unit used to measure magnetic field). To make the conversion, we must consult Table 9 (Gain Settings) of the IC’s datasheet. Since the Gain was left as default, we are using a gain of 1090. We can simply divide each raw count by the gain to convert the count into units of Gauss.
The last thing to do before we output the data to the Serial Monitor is to calculate the heading. If the device is oriented with the X-Y plane parallel to the ground, we can use the X and Y values to obtain the vector that indicates the heading. This is done with the function getHeading(). The arctangent is calculated between the (X, Y) point and the x-axis, as shown in the image below. The reason we use atan2 is to be able to calculate the arctangent in all four quadrants.
atan2 returns the angle in radians, so we can convert this to degrees. A compass only gives a heading from 0 to 360 degrees, and if we get a heading outside of this range, we can add 360 if it is negative or subtract 360 if it is greater than 360. Now that the data has been normalized and the heading has been calculated, we can display it via the Serial Monitor.
//Interface a HMC5883L 3-axis digital compass to an Arduino Uno
/*Copyright (c) 2019, ProteShea LLC
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holders nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <Wire.h>
//Address map for registers
#define configA 0x00
#define configB 0x01
#define mode 0x02
#define dataOutX_U 0x03
#define dataOutX_L 0x04
#define dataOutZ_L 0x05
#define dataOutZ_L 0x06
#define dataOutY_L 0x07
#define dataOutY_L 0x08
#define statusReg 0x09
//Operating modes sent to Mode register (0x02)
#define continuous 0x00
#define single 0x01
#define idle 0x02
#define i2c_addr 0x1E
#define gain 1090
int16_t x = 0;
int16_t y = 0;
int16_t z = 0;
float heading;
float gaussX;
float gaussY;
float gaussZ;
void setup() {
Wire.begin();
Serial.begin(9600);
setOperatingMode(continuous);
setSamples();
}
void loop() {
getXYZ();
convert(x,y,z);
getHeading(gaussX,gaussY,gaussZ);
Serial.print("X: ");
Serial.print(gaussX);
Serial.print(" Y: ");
Serial.print(gaussY);
Serial.print(" Z: ");
Serial.println(gaussZ);
Serial.print("Heading: ");
Serial.println(heading);
delay(500);
}
//Convert the raw X, Y, Z counts to Gauss
void convert(int16_t rawX, int16_t rawY, int16_t rawZ){
gaussX = (float)rawX/gain;
gaussY = (float)rawY/gain;
gaussZ = (float)rawZ/gain;
}
//accounts for declination (error in magnetic field which is dependent on location)
void getHeading(float X, float Y, float Z){
heading = (atan2(Y,X) - 0.1) * 180 / PI;
if (heading < 0) heading += 360;
if (heading > 360) heading -= 360;
}
void setSamples(void){
Wire.beginTransmission(i2c_addr);
Wire.write(configA); //write to config A register
Wire.write(0x70); //8 samples averaged, 15Hz output rate, normal measurement
Wire.endTransmission();
delay(10);
}
void setOperatingMode(uint8_t addr){
Wire.beginTransmission(i2c_addr);
Wire.write(mode); //write to mode register
Wire.write(addr); //set measurement mode
Wire.endTransmission();
delay(10);
}
//get the raw counts of X, Y, Z from registers 0x03 to 0x08
void getXYZ(void){
Wire.beginTransmission(i2c_addr);
Wire.write(0x03);
Wire.endTransmission();
Wire.requestFrom(i2c_addr, 6);
if (Wire.available() >= 6){
int16_t temp = Wire.read(); //read upper byte of X
x = temp << 8;
temp = Wire.read(); //read lower byte of X
x = x | temp;
temp = Wire.read(); //read upper byte of Z
z = temp << 8;
temp = Wire.read(); //read lower byte of Z
z = z | temp;
temp = Wire.read(); //read upper byte of Y
y = temp << 8;
temp = Wire.read(); //read lower byte of Y
y = y | temp;
}
}
The next example displays the data on the 20×4 LCD by calling the function writeLCD().
//Interface a HMC5883L 3-axis digital compass to an Arduino Uno and display
//data on 20x4 LCD
/*Copyright (c) 2019, ProteShea LLC
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holders nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <Wire.h>
#include <LiquidCrystal.h>
//Address map for registers
#define configA 0x00
#define configB 0x01
#define mode 0x02
#define dataOutX_U 0x03
#define dataOutX_L 0x04
#define dataOutZ_L 0x05
#define dataOutZ_L 0x06
#define dataOutY_L 0x07
#define dataOutY_L 0x08
#define statusReg 0x09
//Operating modes sent to Mode register (0x02)
#define continuous 0x00
#define single 0x01
#define idle 0x02
#define i2c_addr 0x1E
#define gain 1090
const int RS = 2, EN = 3, D4 = 4, D5 = 5, D6 = 6, D7 = 7;
LiquidCrystal lcd(RS,EN,D4,D5,D6,D7); //set Uno pins that are connected to LCD, 4-bit mode
int16_t x = 0;
int16_t y = 0;
int16_t z = 0;
float heading;
float gaussX;
float gaussY;
float gaussZ;
void setup() {
Wire.begin();
lcd.begin(20,4); //set 20 columns and 4 rows of 20x4 LCD
setOperatingMode(continuous);
setSamples();
}
void loop() {
getXYZ();
convert(x,y,z);
getHeading(gaussX,gaussY,gaussZ);
writeLCD();
delay(500);
}
void writeLCD(void){
lcd.clear();
lcd.print("X: ");
lcd.print(gaussX);
lcd.setCursor(0,1);
lcd.print("Y: ");
lcd.print(gaussY);
lcd.setCursor(0,2);
lcd.print("Z: ");
lcd.print(gaussZ);
lcd.setCursor(0,3);
lcd.print("Heading: ");
lcd.print(heading);
}
//Convert the raw X, Y, Z counts to Gauss
void convert(int16_t rawX, int16_t rawY, int16_t rawZ){
gaussX = (float)rawX/gain;
gaussY = (float)rawY/gain;
gaussZ = (float)rawZ/gain;
}
//accounts for declination (error in magnetic field which is dependent on location)
void getHeading(float X, float Y, float Z){
heading = (atan2(Y,X) - 0.1) * 180 / PI;
if (heading < 0) heading += 360;
if (heading > 360) heading -= 360;
}
void setSamples(void){
Wire.beginTransmission(i2c_addr);
Wire.write(configA); //write to config A register
Wire.write(0x70); //8 samples averaged, 15Hz output rate, normal measurement
Wire.endTransmission();
delay(10);
}
void setOperatingMode(uint8_t addr){
Wire.beginTransmission(i2c_addr);
Wire.write(mode); //write to mode register
Wire.write(addr); //set measurement mode
Wire.endTransmission();
delay(10);
}
//get the raw counts of X, Y, Z from registers 0x03 to 0x08
void getXYZ(void){
Wire.beginTransmission(i2c_addr);
Wire.write(0x03);
Wire.endTransmission();
Wire.requestFrom(i2c_addr, 6);
if (Wire.available() >= 6){
int16_t temp = Wire.read(); //read upper byte of X
x = temp << 8;
temp = Wire.read(); //read lower byte of X
x = x | temp;
temp = Wire.read(); //read upper byte of Z
z = temp << 8;
temp = Wire.read(); //read lower byte of Z
z = z | temp;
temp = Wire.read(); //read upper byte of Y
y = temp << 8;
temp = Wire.read(); //read lower byte of Y
y = y | temp;
}
}
Comments