Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / IoT / Raspberry-Pi

Calculating True North for IoT Applications

4.97/5 (52 votes)
11 Aug 2016MIT12 min read 92.5K  
Finding true North using GPS and a compass with Raspberry Pi.

Introduction

Finding your geographic location and direction is useful in robotics and embedded systems. Using GPS and a Compass Sensor it's easy to determine your location and your direction in relation to Magnetic North. But what if you want to know you direction in relation to True North?

It's not difficult to determine your offset from Magnetic North to True North, also known as Magnetic Declination. Magnetic Declination can be easily found on the web through a NOAA website. But I wanted a real time model that I could use in my embedded device through code, enter the World Magnetic Model (WMM)! Using GPS with the WMM in Linux can easily determine Magnetic Declination that you can then use to offset the Magnetic Bearing from your Compass. I will take you through the steps to determine Magnetic Declination and use it to adjust your compass  bearing.

Image 1

Background

I'm a LEGO maker and a winner in a LEGO prototype challenege. I have been working on a star finding camera robot made out of LEGO and Raspberry Pi components. To determine stellar coordinates I first need to know my robot's geographic coordinates and which direction it's facing in relation to true North with a reasonable degree of accuracy. Setting up the GPS and Compass was fairly straightforward and installing and accessing the World Magnetic Model was also fairly simple. 

This is my first CodeProject article and my robot is still a work in progress but has made it a long way. I wanted to share some of the work I've already done with the community. I hope you like it!

Image 2Image 3

You can find more images of my robot and my other LEGO devices at these links.

Starbot (Lego Camera)
Lego Digital Picture Frame
Point & Shoot IR Camera
Lego Computer

Components Used

You'll need to setup the following devices, instructions for setup are included in the links below. I will not cover these topics in detail so you'll need to be somewhat familiar with Raspberry Pi setup and Digital and I2C sensors in linux.

1 Raspberry Pi 2 Model B running Raspbian.

Adafruit Ultimate GPS Breakout (Setup Instructions)

HMC5883L Triple Axis Magnetometer (Setup Instructions)

Wiring Diagram

Image 4

Required Libraries

You'll need a few libraries installed on your Raspberry Pi, these will give you access to your GPS and Magnetic Models through code. The compass does not require additional libriaries but it does use i2c-dev.h.

GPSd (Setup Instructions) (Access to GPS data using libgpsmm.h)

GeographicLib (Setup Instructions) (Access to WMM data and additional Geographic related algorithms)

Reading GPS Coordinates

The first step is to read the GPS coordinates from your device. Using GPSd and libgpsmm.h make it pretty simple to access your GPS data. We're going to make a class to manage this interface and supply us with values streamed from the device. The GPS uses a serial connection and the sentences returned are parsed for you by libgps. We can either use the raw coordinate values returned or they can be broken into components like degrees, minutes and seconds. 

The GPS Class

I created a simple wrapper class to manage interactions with the GPS library and do coordinate conversions between raw and component values.

C++
#ifndef __GPS_H_INCLUDED__
#define __GPS_H_INCLUDED__

#include "libgpsmm.h" // GPS

class gps
{
private:
   // Main entry into GPSd.
	gpsmm *gps_rec; 

    // Private Helper functions for Converting Values.
	int degrees(float coordinate);
	int minutes(float coordinate);
	float seconds(float coordinate);
	
public:
    // Values for our Application to Consume.
	int							gps_fix;
	float						gps_lon;
	float						gps_lat;
	float						gps_alt;
	int							gps_sat;
	int							gps_use;
  
    // Routines for managing interactions with GPSd.

	bool start(); // Starts GPSd interface

	bool update(); // Retrieves Coordinates from GPSd.

	bool stop(); // Shuts down GPSd interface and releases resources.


    // Conversion functions to return latitude components from raw GPS values.
	int latitude_degrees();
	int latitude_minutes();
	float latitude_seconds();
	
    // Conversion functions to return longitude components from raw GPS values.
	int longitude_degrees();
	int longitude_minutes();
	float longitude_seconds();
};

#endif

Initializing GPS Stream

Now that your class is setup you'll need to initialize your GPS stream. It's only a few lines of code, you'll be able to modify a few options here, GPS device address and stream formatting, but I used the defaults. You can do additional research on the GPSd site to determine how to use the parameters.

C++
bool gps::start() 
{
	/* Initialize GPS */
	gps_rec = new gpsmm("localhost", DEFAULT_GPSD_PORT);
   
    /* Setup GPS Stream */
	if (gps_rec->stream(WATCH_ENABLE | WATCH_JSON) == NULL) 
    {
		return false;
	}

	return true;
}

Reading GPS Stream

Now that your device is setup you can read the values from the stream. This is also pretty simple to do, I created a function called update() that is then called once per frame of my main control loop. We'll use a data structure called gps_data_t provided by libgpsmm.h to store the raw values and then store them in our class level variables. gps_data_t returns a lot of values, you may wish refer to the online documentation to see if there are additional values you may need to use for your purpose.

C++
bool gps::update() 
{
    /* Raw GPS Values structure */
	struct gps_data_t* gps_data;
	
    /* Wait until device is ready */
	if (gps_rec->waiting(00050000)) 
    {
		/* See if we're reading data. */
		if ((gps_data = gps_rec->read()) == NULL) 
        {
			return false;
		}
		
        /* Check for a 2D or 3D fix. */
		if (gps_data->status == STATUS_FIX && 
           (gps_data->fix.mode == MODE_2D || gps_data->fix.mode == MODE_3D)) 
        {
			/* Fix Obtained, Set Values. */
			gps_fix = gps_data->fix.mode;
			gps_lon = gps_data->fix.longitude;
			gps_lat = gps_data->fix.latitude;
			gps_alt = gps_data->fix.altitude;

			gps_sat = gps_data->satellites_visible;
			gps_use = gps_data->satellites_used;
		}
	}
	
	return true;
}

Shutting Down GPS

When you're done you need to shutdown your stream and release your resources. 

C++
bool gps::stop() 
{
	if(gps_rec->stream(WATCH_DISABLE) == NULL) 
    {
		delete gps_rec;
		return false;
	}
	
	delete gps_rec;
	return true;
}

That's it, as you can see it's pretty simple to setup and read coordinates from your GPS device. Now that you have raw coordinate values you may want to parse the component values from the decimal representation. The raw values represents the Degrees, Minutes, and Seconds components as a single decimal value.

Parsing Degrees

Degress are represented as the whole number part of the raw value, in order to determine the degress just truncate the floating point values. Here I just cast to an int and return the absolute value.

C++
/* 
 * degrees: converts decimal angle to degree component. 
 * 
 */
int gps::degrees(float angle) 
{
	return abs((int)angle);
}

Parsing Minutes

Minutes are one sixtyth of one degree. Once you remove you degree component from your raw value you're left with minutes and seconds. To convert to minutes you'll need to multiply by 60 and then truncate the floating point values to drop the seconds. 

C++
/*
* minutes: converts decimal angle to minutes component.
*
*/
int gps::minutes(float angle) 
{
	int deg = degrees(angle);
	int min = (int)((fabs(angle) - deg) * 60.00);

	return min;
}

Parsing Seconds

Seconds are one sixtyth of one minute. To get the minutes you'll need to do two steps, First you remove the degrees from the raw coordinate value, next you perform the step to convert to minutes. Now that you have minutes you can deduct the whole value and again mulitply by 60, this will leave you with your seconds component. Seconds can be returned as a decimal value with fractional values.

C++
/*
* seconds: converts decimal angle to seconds component.
*
*/
float gps::seconds(float angle) 
{
	int deg = degrees(angle);
	int min = minutes(angle);

	float sec = (((fabs(angle) - deg) * 60.00) - (float)min) * 60.00;

	return sec;
}

Determining East vs West and North vs South

There is an additional component encoded into the raw value of your gps coordinates. This value determines East/West for Longitude and North/South for Latitude. This is encoded into the sign bit of the raw GPS value. if your original value is negative this will indicate West/South and if it is positive it will indicate East/North. 

C++
gps_lat >= 0 ? 'N' : 'S'; // Determine North or South.
gps_lon >= 0 ? 'E' : 'W'; // Determine East or West.

Example Data

Decimal Latitude: -15.063888888888888
Converted Latitude: 15° 3' 50" W

Decimal Longitude: 45.33416666666667
Converted Longitude: 45° 20' 3" N

World Magnetic Model and Declination

Now that we have our GPS coordinates the next step is to determine our robot's Magnetic Declination. We can do this with GeographicLib and the World Magnetic Model but first we need to understand the concept of Magnetic Declination.

Declination

Quote: https://en.wikipedia.org/wiki/Magnetic_declination
Image 5

Magnetic declination or variation is the angle on the horizontal plane between magnetic north (the direction the north end of a compass needle points, corresponding to the direction of the Earth's magnetic field lines) and true north (the direction along a meridian towards the geographic North Pole). This angle varies depending on position on the Earth's surface, and changes over time.
 

As you can see Magnetic declination is simply the angle of offset between Magnetic North and True North. This value changes with position and time. It's important to make sure to refresh this value since it could vary between frames in your control loop (It's unlikely but possible). There is also a similar concept called inclination, this determines the angle of the magnetic field on the vertical plane but we don't need this to determine our offset from Magnetic North.

Magnetic Model

To determine Magnetic Declination we need to be able to understand the magnetic field lines at our robot's location. From the description we know that Magnetic Declination can change with position and time. So how would we know what our actual declination is if it's constantly changing? The World Magnetic Model is the answer, it is a precomputed data set that is provided by NOAA and can be consumed by GeographicLib.  The datasets provided are usually good for a few years at a time and need to be updated with regularity to ensure accurate information.

Image 6

You can find more information and useful tools at the NOAA website

Using GeographicLib

I created a simple wrapper around the GeographicLib library similar to the GPS wrapper. This has properties for the values we wish to query, such as declination and inclination. This code is actually easier to use then the GPS component and does not require a start() or stop() step.

C++
#ifndef __WMM_H__
#define __WMM_H__

#include <GeographicLib/MagneticModel.hpp> // Magnetic Model

using namespace GeographicLib;

class wmm
{
private:
	double magnetic_declination;
	double magnetic_inclination;
	double field_strength;

public:

	double declination();
	double inclination();
	double strength();

	void update(float lat, float lon, float alt);
};
#endif // !__WMM_H__

Getting Declination

We'll need to use the latitude, longitude and altitude returned by GPS Module for our update(...) function. This will give us a 3D position to determine the local magnetic field produced by the Earth. Additionally we also need to determine the current date and time to return accurate Magnetic Model information for the current moment.

C++
void wmm::update(float lat, float lon, float alt)
{
    /* intermediate values */
	double Bx, By, Bz, H;
    
    /* Determine current time. */
	time_t t = time(NULL);
	tm* timePtr = localtime(&t);

    /* 
     * Magnetic Model component, Using emm2015 magnetic model 
     * (emm2015 includes magnetic interactions from Earth's crust)
     */
	MagneticModel mag("emm2015");

    /* Get intermediate values using current time and position */
	mag(timePtr->tm_year + 1900, lat, lon, alt, Bx, By, Bz);

    /* Convert intermediate values into field components and store in class members */
	MagneticModel::FieldComponents(Bx, By, Bz, H, 
        field_strength, magnetic_declination, magnetic_inclination);
}

Reading Compass Bearings

Our compass uses the i2c interface and does not require a library outside of i2c-dev. This makes it a little more complicated to setup than our GPS but not by much. The value returned should be the local Magnetic Bearing. This is the value we will need to offset to determine the bearing for True North. Magnetometers are prone to noise and may not return a consistent value over time due to magnetic interference from local magnetic fields. This can be due to other metals or electronics near your sensor or in my case the length of my jumper wires. You will need to ensure that your compass sensor is as isolated from random magnetic fields as possible.

Compass Class

The compass class is pretty simple, it interacts with i2c and then filters the values using a Kalman filter for smoother output.

C++
#ifndef __COMPASS_H__  
#define __COMPASS_H__

#include "kalman.h"

#define HMC5883L_I2C_ADDR 0x1E

class compass
{
private:
	int i2c_fd;
	
    /* i2c interaction functions */
	bool select_i2c_device(int fd, int addr, char * name);
	bool write_to_i2c(int fd, int reg, int val);

public:
    /* Filtered Compass Bearing */
	float bearing;

    /* Routines for managing interactions with the compass sensor */
	int start();
	int update();
};

#endif

Initializing the Compass

To initialize the compass we'll first need to select our i2c device and then write some data to the bus using the following select_i2c_device(...) and write_to_i2c(...) functions.

/* Select our i2c compass device */
bool compass::select_i2c_device(int fd, int addr, char * name) 
{
    if (ioctl(fd, I2C_SLAVE, addr) < 0) {		
		return false;
    }
	
	return true;
}

/* Write something to the i2c device */
bool compass::write_to_i2c(int fd, int reg, int val) 
{
    char buf[2];
    
	buf[0]=reg;
    buf[1]=val;
	
    if (write(fd, buf, 2) != 2) {
		return false;
    }

	return true;
}

Now in our start() function we can call the previous i2c functions to setup our device.

int compass::start()
{
	init = true;
	
	/* Initialize i2c compass */
	if ((i2c_fd = open("/dev/i2c-1", O_RDWR)) < 0) 
    {
		return 0;
    }

    /* initialise HMC5883L */
    select_i2c_device(i2c_fd, HMC5883L_I2C_ADDR, "HMC5883L");
	
    /* Set Initial register values*/
    write_to_i2c(i2c_fd, 0x01, 32);
    write_to_i2c(i2c_fd, 0x02, 0);
	
	return 1;
}

Reading Compass Bearing

The compass should now be ready to read from. Like the GPS and WMM we want to keep updating our sensor value in case our position or orientation change, similar to the other components we will use the update() function.

C++
int compass::update()
{
	float angle = 0;
	unsigned char i2c_buf[16];
	i2c_buf[0] = 0x03;

    /* Send the register to read from */
    if ((write(i2c_fd, i2c_buf, 1)) != 1) 
    {
        
		return 0;
    }

    /* Read from the register values. */
    if (read(i2c_fd, i2c_buf, 6) != 6) 
    {
		return 0;
    } 
    else 
    {
        /* Convert raw byte values to shorts. */
        short x = (i2c_buf[0] << 8) | i2c_buf[1];
        short y = (i2c_buf[4] << 8) | i2c_buf[5];
        
        /* Determine Compass Bearing Angle from raw x and y components. */
        angle = atan2(y, x) * 180 / M_PI;
	}
	
    /* Set the filtered Bearing. */
	bearing = angle;
	
	return 1;
}

Here you see us reading from the i2c buffer and then converting the byte values into an angle. You'll also see that we're using something called a Kalman filter. This will reduce the amount of noise returned by the compass sensor (More on this in the next section).

Filtering Compass Values

The compass values are inherently noisy, due to interference from local electromagnetic sources and physical motion. You may notice that your value does not remain constant after subsequent reads, though it should be fairly consistent without large jumps between values. However, it's better to smooth these values over time for a cleaner reading. There are multiple filters that could be used but I chose a simple kalman filter the original implimentation can be found here.

Quote: https://en.wikipedia.org/wiki/Kalman_filter

Kalman filtering, also known as linear quadratic estimation (LQE), is an algorithm that uses a series of measurements observed over time, containing statistical noise and other inaccuracies, and produces estimates of unknown variables that tend to be more precise than those based on a single measurement alone, by using Bayesian inference and estimating a joint probability distributionover the variables for each timeframe. The filter is named after Rudolf E. Kálmán, one of the primary developers of its theory.

Filtering

Below is a graph that shows how the filtered values look in relation to the raw values. The darker line represents the noisy value returned from the sensor and the lighter line represents the smoothed values. As you can see the smoothed values will be more consitent than the unfiltered values. The values returned by filtering tend to be more uniform over time, it's important to keep in mind that we're making an prediction about where our value should be and it can slightly lag behind your raw value. This means that it might take slightly longer to get a "fix" on your target value while processing.

Image 7

Kalman.h

There was no need to make a wrapper class for this since we're just storing a few values and then updating them. We have a struct that stores our state and two functions a kalman_init(...) and a kalman_update(...)

C++
#ifndef __KALMAN_H__  
#define __KALMAN_H__

typedef struct {
	float q; //process noise covariance
	float r; //measurement noise covariance
	float x; //value
	float p; //estimation error covariance
	float k; //kalman gain
} kalman_state;

kalman_state kalman_init(float q, float r, float p, float x);

void kalman_update(kalman_state *s, float m);

#endif // __KALMAN_H__

Initializing the Kalman Filter

It's pretty simple to initialize, we create a new kalman state struct and then just set the values of the state to those passed into the function. We pass in a Process Noise Covariance (q), Measurement Noise Covariance (r), Estimation Error Covariance (p), and an initial value (x)

C++
kalman_state kalman_init(float q, float r, float p, float x) {
	kalman_state s;
	s.q = q;
	s.r = r;
	s.p = p;
	s.x = x;

	return s;
}

In our control loop we will initialize the kalman filter using the following lines of code. The values I chose here were estimates, I just adusted them until the values returned seemed reasonably smooth. 

C++
/* Initialize Kalman filter on first call only */
if(init) 
{
    state = kalman_init(0.025f, 16, 1, compass_sensor->bearing);
    init = false;
}

Updating the Kalman Filter

To do an update, we pass in the previous state and the new measured value from the compass. First we need to make a prediction based on earlier states and then we update the measurement values.

void kalman_update(kalman_state * s, float m) {
	//prediction update
	//omit x = x
	s->p = s->p + s->q;

	//measurement update
	s->k = s->p / (s->p + s->r);
	s->x = s->x + s->k * (m - s->x);
	s->p = (1 - s->k) * s->p;
}

Finally, in our main control loop we call the update function for the kalman filter everytime we update the compass.

/* Update Filtered Bearing */
kalman_update(&state, compass_sensor->bearing);

/* Store Filtered Bearing */
float filtered_bearing = state.x;

That's it, we're now ready to put it all together and get our final Bearing for True North.

Putting It Together

Once all of the components are ready we can now build our main control loop. In this example I'm just printing the values to screen using an infinite loop. You'll likely want to create a well timed control loop for your purposes.

Initialization 

We start by creating and initializing the GPS, WMM, Compass and Kalman filter components. Next we call any start() methods before entering the main loop.

C++
/* Setup Kalman Filter */
 bool init = true;
kalman_state state;

/* Create Components */
gps * gps_sensor = new gps();
wmm * magnetic_model = new wmm();
compass * compass_sensor = new compass();

/* Initialize Components */
gps_sensor->start();
compass_sensor->start();

Control Loop

Once we enter the main control loop you just update the GPS sensor, pass longitude, latitude and altitude values to the world magnetic model. Now we read from the compass, intialize the kalman filter and then filter the values.

C++
/* Enter Main Control Loop */
while (1)
{
    /* Update GPS Sensor */
    gps_sensor->update();

    /* Pass GPS Data to WMM to get Declination */
    magnetic_model->update(gps_sensor->gps_lat, gps_sensor->gps_lon, gps_sensor->gps_alt);

    /* Read from Compass Sensor */
    compass_sensor->update();

   /* Initialize the Kalman filter */
   if (init)
   {
       state = kalman_init(0.025f, 16, 1, compass_sensor->bearing);
       init = false;
   }

  /* Update Filtered Bearing */
  kalman_update(&state, compass_sensor->bearing);

  /* Store Filtered Bearing */
  float filtered_bearing = state.x;
}

True North

Once we have obtained the filtered compass value we can then use it to determine the offset to our bearing using the following line of code. It's a very simple calculation, you just add the declination offset to your filtered bearing value. 

C++
float true_bearing = filtered_bearing + magnetic_model->declination();

Printing Values

I'm just printing the values to the screen in this demo. However, in my case I use these values to help control a servo motor to control the pan of my telescope.  You would want to use these values to control your device instead of just printing values, this is where you would perform any additional steps needed.

C++
/* Reset Terminal Output */
printf("\033[0;0H");

/* Print GPS Coordinates */
printf("Latitude: %3d° %2d' %2.3f\" %c Longitude: %3d° %2d' %2.3f\" %c\n\r",
    gps_sensor->latitude_degrees(),
    gps_sensor->latitude_minutes(),
    gps_sensor->latitude_seconds(),
    gps_sensor->gps_lat >= 0 ? 'N' : 'S',
    gps_sensor->longitude_degrees(),
    gps_sensor->longitude_minutes(),
    gps_sensor->longitude_seconds(),
    gps_sensor->gps_lon >= 0 ? 'E' : 'W');

/* Print Altitude and Declination */
printf("Altitude: %2.3f Declination: %2.3f",
    gps_sensor->gps_alt),
    magnetic_model->declination());

/* Print Raw Bearing, Filtered Bearing and True Bearing */
printf("Magnetic Bearing: %2.3f  Filtered Bearing %2.3f True Bearing %2.3f\n\r",
    compass_sensor->bearing,  
    filtered_bearing,
    true_bearing);

Cleaning Up

Once you exit the main loop you want to remember to stop the GPS and cleanup your components.

C++
/* Stop GPS sensor */
gps_sensor->stop();

/* Cleanup Components */
delete gps_sensor;
delete compass_sensor;
delete magnetic_model;

Complete Example

C++
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "wmm.h"
#include "compass.h"
#include "gps.h"
#include "kalman.h"

int main()
{
    bool init = true;
    kalman_state state;

    printf("starting...\n\r");

    /* Create Components */
    gps * gps_sensor = new gps();
    wmm * magnetic_model = new wmm();
    compass * compass_sensor = new compass();

    /* Initialize Components */
    gps_sensor->start();
    compass_sensor->start();

    /* Clear Screen */
    printf("\033[2J\033[?25l");

    /* Enter Main Control Loop */
    while (1) {
        /* Update GPS Sensor */
        gps_sensor->update();

        /* Pass GPS Data to WMM to get Declination */
        magnetic_model->update(gps_sensor->gps_lat, gps_sensor->gps_lon, gps_sensor->gps_alt);

        /* Read from Compass Sensor */
        compass_sensor->update();

        if (init) {
            state = kalman_init(0.025f, 16, 1, compass_sensor->bearing);
            init = false;
        }

        kalman_update(&state, compass_sensor->bearing);

        float filtered_bearing = state.x;

        /* Calculate True Bearing from compass bearing and WMM Declination */
        float true_bearing = filtered_bearing + magnetic_model->declination();

        /* Reset Terminal Output */
        printf("\033[0;0H");

        /* Print GPS Coordinates */
        printf("Latitude: %3d° %2d' %2.3f\" %c\n\rLongitude: %3d° %2d' %2.3f\" %c\n\r",
            gps_sensor->latitude_degrees(), 
            gps_sensor->latitude_minutes(), 
            gps_sensor->latitude_seconds(), 
            gps_sensor->gps_lat >= 0 ? 'N' : 'S',
            gps_sensor->longitude_degrees(), 
            gps_sensor->longitude_minutes(), 
            gps_sensor->longitude_seconds(), 
            gps_sensor->gps_lon >= 0 ? 'E' : 'W');

        /* Print Altitude and Declination */
        printf("Altitude: %2.3f\n\r\n\rDeclination: %2.3f\n\r", 
            gps_sensor->gps_alt, 
            magnetic_model->declination());

        /* Print Raw Bearing, Filtered Bearing and True Bearing */
        printf("\n\rMagnetic Bearing: %2.3f\n\rFiltered Bearing: %2.3f\n\rTrue Bearing %2.3f\n\r", 
            compass_sensor->bearing,  
            filtered_bearing, 
            true_bearing);
    }

    gps_sensor->stop();

    delete gps_sensor;
    delete compass_sensor;
    delete magnetic_model;

    return 0;
}

Using the Code

If you've set up your hardware following the diagram and the software prerequisites at the begging of the article you can download and run the sample using the following bash commands.

BAT
git clone https://github.com/GalacticSoft/Starbot-Command.git
cd Starbot-Command/src
make clean
make demo
sudo ./demo

That's it, if the demo works you should see your coordinates printed to the screen like the screenshot below.

Image 8

Now you're ready to start using geographic location and compass bearings with your robot or IoT device, enjoy!

License

This article, along with any associated source code and files, is licensed under The MIT License