Raspberry Pi Digital compass

Create a Digital Compass with the Raspberry Pi – Part 1 – “The Basics”

This will be a multipart series on how to use a digital compass(magnetometer) with your Raspberry Pi.

The magnetometer used in these tutorials is a LSM9DS0 which is on a BerryIMU. We will also point out where some of the information can be found in the Datasheet for the LSM9DS0. This will help you understand how the LSM9DS0 works.

The math and logic in this series can also be used with other magnetometers or IMUs.

We will also go over how to do some basic communication on the i2c bus. As well as using SDL to display the compass heading as traditional compass as shown in the video above.

Git repository here
The code can be pulled down to your Raspberry Pi with;

pi@raspberrypi ~ $ git clone https://github.com/ozzmaker/BerryIMU.git

The code for this guide can be found under the compass_tutorial01_basics directory. 

Overview of a Compass

Raspberry Pi Compass
A traditional Magnetic compass (as opposed to a gyroscopic compass) consists of a small, lightweight magnet balanced on a nearly frictionless pivot point. The magnet is generally called a needle. The Earth’s Magnetic field will cause the needle to point to the North Pole.

To be more accurate, the needle points to the Magnetic North. The angle difference between true North and the Magnetic North is called declination. Declination is different in different locations. This angle varies depending on position on the Earth's surface, and changes over time.

The strength of the earth's magnetic field is about 0.5 to 0.6 gauss .

There is also  a component parallel to the earth's surface that always points toward the magnetic north pole. In the northern hemisphere, this field points down. At the equator, it points horizontally and in the southern hemisphere, it points up. This angle between the earth’s magnetic field and the horizontal plane is defined as an inclination angle.

Magnetometer, is usually a microelectromechanical system (MEMS) instrument for measuring the strength and the direction of a magnetic field on 3 axis.

Connecting the magnetometer to the Raspberry Pi

The image below shows how to wire up the magnetometer or BerryIMU to the Raspberry Pi.

Raspberry Pi BerryIMU

Enable i2c on the Raspberry Pi

The components on the BerryIMU talk to the Raspberry Pi via i2c.

You can follow this guide to enable i2c on your Raspberry Pi

BerryIMU Raspberry Pi Gyroscope Accelerometer

i2c is a communication protocol that runs over a two wire bus. The two wires are called SDA (Serial Data) and SCL (Serial Clock). The i2c bus has one or more masters (the Raspberry Pi) and one or more slave devices, like LSM9DS0 on the BerryIMU. As the same data and clock lines are shared between multiple slaves, we need some way to choose which device to communicate with. With i2c, every device has an address that each communication must be prefaced with. The LSM9DS0 can have two addresses, either 0x1E or 0x1D.  This address is controlled by one pin on the LSM9DS0 . If this pin is connected to a voltage supply, the address will be 0x1D. And if connected to ground, it will be 0x1E.
On the BerryIMU the address of LSM9DS0 is 0x1E.

This information can be found on page 33 of the datasheet.

i2cdetect can be used to confirm the addresses used by the magnetometer or BerryIMU

pi@raspberrypi ~ $ sudo /usr/sbin/i2cdetect -y 1

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- 1e -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- 6a -- -- -- --
70: -- -- -- -- -- -- -- --

In the above example the magnetometer (LSM303D) is using the address of 0x1D.

We read and write values to different registers on the magnetometer to perform different tasks. Eg Turn the magnetometer on , set the sensitivity level, read magnetic values. etc..

The git repository also includes LSM9DS0.h which lists all the registers we use for this sensor.

Open the I2C Bus

We use ioctl calls to read and write information to the i2c bus. So the first task to do is we need to open the I2C bus device file, as follows;

        char filename[20];
        sprintf(filename, "/dev/i2c-%d", 1);
        file = open(filename, O_RDWR);
        if (file<0) {
                printf("Unable to open I2C bus!");
                exit(1);
        }

As we are using sprintf, you will need to include the header file stdio.h

Selecting the magnetometer
Before we can start writing and reading values form the magnetometer, we need to select the address of the magnetometer;

        if (ioctl(file, I2C_SLAVE, MAG_ADDRESS) < 0) {
                printf("Error: Could not select magnetometer\n");
        }

You will need to include the file control headers in your program, fcntl.h.
You will also need to include the headers for the i2c bus - linux/i2c-dev.h

MAG_ADDRESS is defined in LSM9DS0.h

Enable the magnetometer

To enable the magnetometer, we will need to set some register values on the sensor.

The i2c_smbus_write_byte_data() function can be used to write to a device's register on the i2c bus.
This function can be found in linux/i2c-dev.h headers.

The decleration for this function is;
i2c_smbus_write_byte_data (struct i2c_client * client, u8 command, u8 value);

i2c_client is the pointer we used to open the i2c bus, command is the register we want to write to and value is the value we want to write.

As we will be writing to the magnetometer often, we should create a function;

void writeMagReg(uint8_t reg, uint8_t value)
{
  int result = i2c_smbus_write_byte_data(file, reg, value);
    if (result == -1)
    {
        printf ("Failed to write byte to I2C Mag.");
        exit(1);
    }
}

You can now go ahead and enable the magnetometer;

 writeMagReg( CTRL_REG5_XM, 0b11110000);
 writeMagReg( CTRL_REG6_XM, 0b01100000);
 writeMagReg( CTRL_REG7_XM, 0b00000000);

A brief description of the settings and where the information can be found in the datasheet;

  • CTRL_REG5_XM- Enable internal temperature sensor - Set magnetometer to high resolution - set a datarate of 50Hz - page 59 on the datasheet.
  • CTRL_REG6_XM - Will set the full scale selection to +/- 12 Gauss - page 60 on the datasheet.
  • CTRL_REG7_XM - Se the magnetometer to Continuous-conversion mode - page 60 on the datasheet.

Reading Raw Values from the Magnetometer

Reading data from the magnetometer is similar to writing data, but rather than just reading one byte we will be reading a block of bytes. E.g. 6 bytes.

We will use the i2c_smbus_read_i2c_block_data() function found in linux/i2c-dev.h.

We will create two of our own functions which we will be used to read data from the magnetometer.

This is the read block function which also performs error checking;

void  readBlock(uint8_t command, uint8_t size, uint8_t *data)
{
    int result = i2c_smbus_read_i2c_block_data(file, command, size, data);
    if (result != size)
    {
       printf("Failed to read block from I2C.");
        exit(1);
    }
}

This function uses the above function to read raw magnetic values from the magnetometer;

void readMAG(int  *m)
{
        uint8_t block[6];
        readBlock(0x80 | OUT_X_L_M, sizeof(block), block);
        *m = (int16_t)(block[0] | block[1] << 8);
        *(m+1) = (int16_t)(block[2] | block[3] << 8);
        *(m+2) = (int16_t)(block[4] | block[5] << 8);
}

Here is what is happening in the readMAG() function;
1) An array of 6 bytes is first created to store the values.
2) Using the readBlock() function, we read 6 bytes starting at OUT_X_L_M (0x08). This is shown on page 52 of the datasheet.
3) The values are expressed in 2’s complement (MSB for the sign and then 15 bits for the value) so we need to combine;
block[0] & block[1] for X axis
block[2] & block[3] for Y axis
block[4] & block[5] for Z axis

Print Raw values

We can now create a loop within our program to read the raw values from the magnetometer and print them to screen.
I have added a delay of 0.25 seconds to make the output easier to read.

int magRaw[3];
while(1)
{
        readMAG(magRaw);
	printf("magRaw X %i    \tmagRaw Y %i \tMagRaw Z %i \n", magRaw[0],magRaw[1],magRaw[2]);
        usleep(250000);
}

The output should be similar to the output below. You will notice that the output changes even though the magnetometer is not moving. This is because all magnetometers are noisy. In a future post, we will show how to filter this noise out.

magRaw X 1024    magRaw Y 1024   MagRaw Z 17920
magRaw X 1280    magRaw Y 256    MagRaw Z 18176
magRaw X 1280    magRaw Y 512    MagRaw Z 17152
magRaw X 512     magRaw Y -513   MagRaw Z 16384
magRaw X 1792    magRaw Y -1     MagRaw Z 18432
magRaw X 512     magRaw Y -257   MagRaw Z 17408
magRaw X 256     magRaw Y -257   MagRaw Z 16384
magRaw X 1280    magRaw Y -257   MagRaw Z 17920
magRaw X 1024    magRaw Y 768    MagRaw Z 17920
magRaw X 768     magRaw Y -513   MagRaw Z 16640
magRaw X 1536    magRaw Y -257   MagRaw Z 18432
magRaw X 768     magRaw Y -1     MagRaw Z 18176
magRaw X 1024    magRaw Y -257   MagRaw Z 17664

Calculate Heading

We can use the below formula to calculate the heading. This only works with the magnetometer is on a flat surface.

 heading = 180 * atan2(magRawY,magRawX)/M_PI;

You will need to include the math header file in your program, math.h and when compiling you will also need to link the math library. '-lm'

With just the above formula, we will get the heading between the values -180 to 180.
We will use the snipped below to change this to a heading value between 0 and 360

        if(heading < 0)
              heading += 360;

We will add the relevant code to our main loop and also print the heading value.

Your main loop should look something like this;

        while(1)
        {
                readMAG(magRaw);
		printf("magRaw X %i    \tmagRaw Y %i \tMagRaw Z %i \n", magRaw[0],magRaw[1],magRaw[2]);
                float heading = 180 * atan2(magRaw[1],magRaw[0])/M_PI;
                if(heading < 0)
                      heading += 360;
                printf("heading %7.3f \t ", heading);
                usleep(250000);
        }

To compile;

pi@raspberrypi ~ $ gcc compass_tutorial01.c -o compass_tutorial01 -lm

your output should now look like this;

heading 118.078          magRaw X -2305  magRaw Y -3073  MagRaw Z 13312
heading 126.873          magRaw X -2305  magRaw Y -1281  MagRaw Z 12800
heading 150.937          magRaw X -769   magRaw Y -2561  MagRaw Z 11776
heading 106.714          magRaw X -2561  magRaw Y -2561  MagRaw Z 12288
heading 135.000          magRaw X -2049  magRaw Y -1537  MagRaw Z 12800
heading 143.126          magRaw X -1537  magRaw Y -1793  MagRaw Z 12288
heading 130.604          magRaw X -2049  magRaw Y -1793  MagRaw Z 12800
heading 138.812          magRaw X -1281  magRaw Y -1793  MagRaw Z 12288
heading 125.544          magRaw X -1537  magRaw Y -1793  MagRaw Z 12288
heading 130.604          magRaw X -2049  magRaw Y -2561  MagRaw Z 13056
heading 128.663          magRaw X -2817  magRaw Y -1537  MagRaw Z 12544
heading 151.382          magRaw X -769   magRaw Y -2817  MagRaw Z 13568

When rotating your magnetometer clockwise, the heading should increase. It should decrease when rotated counter clockwise.
If this is not the case, then you need to convert the Y axis. This will happen if the magnetometer is upside down, like when the BerryIMU is sitting on the Raspberry Pi GPIO headers;

                magRaw[1] = -magRaw[1];

The above line should be added just before the heading calculation is done.

In future posts, we will cover compass calibration, tilt compensation and displaying the heading as a traditional compass using SDL.

Guides and Tutorials

41 thoughts on “Create a Digital Compass with the Raspberry Pi – Part 1 – “The Basics””

  1. would your code work with a HMC5883L magnetometer ?
    according to adafruit it is the same mag as is in the LSM303.
    I have downladed your code and tried to compile it and get several messages about undefined reference to I2C_smbus_read_I2C_block_data.
    on a raspberry pi B+ running latest wheezy with PiTFTr enhancements.
    I dont have the mag installed yet but was trying to compile the code so it will be in place this weekend when I get the mag installed.
    Thank You.

  2. Cool Stuff!!!

    I’ve been playing with an Adafruit compass module connected to a Pi. I display the compass rose and needle with an HTML5 animation using python and the Tornado Websocket modules to push the data to a web browser. Works pretty good, better than I expected.

    I have not implemented tilt compensation in my code yet, though I did find some formulas for the maths, so really I’m looking forward to your next tutorial.

    You can find my notes and code and a link to a youtube video of it working here if you’re interested: http://www.raspberryperl.com/compass/

  3. Hi. Nice tutorial!

    I have got an issue with my IMU (AltIMU-10 v4). When I use the function readBlock I always get the same value in block’s words. That is always 0 for the 1 to 5 words and a constant value for the 6th value. I’m absolutely stuck because the program doesn’t give me any problem neither when I initiate the magnetometer nor when I write the configuration words to it. I have checked the direction registers that I’m using but I don’t find any problem in there. Do you have any idea about what it is happening?

    Thank you.
    Greetings.

      1. Actually I’m defining MAG_ADDRESS as 0x1D because when I define it as (0x3C >> 1), that is 0x1E, I get the problem message “Failed to write byte to I2C Mag” when the configuration words are sent through the writeMagReg function. I have checked these words and these register directions and I don’t find any problem in there.

        This is the configuration that I have got and where I get the problem message when I define MAG_ADDRESS as 0x1E:
        writeMagReg(0x00, 0b10010100); // Temp enable, M data rate = 30 Hz
        writeMagReg(0x01, 0b11100000); // +/- 8.1 gauss
        writeMagReg(0x02, 0b00000000); // Continuous conversion mode

        Any idea about that problem message?

        Thank you very much for your reply!

  4. you may not be enabling the magnetometer properly

    On the LSM303, you have to use CTRL5,CTRL6 and CTRL7 registers.

    writeMagReg(0x24, 0b10010100); // Temp enable, M data rate = 30 Hz
    writeMagReg(0x25, 0b11100000); // +/- 8.1 gauss
    writeMagReg(0x26, 0b00000000); // Continuous conversion mode

    1. Eventually my magnotometer worked properly!

      I wasn’t looking at the correct datasheet. I have got a LSM303D and I thought it was LSM303DLHC.
      The final solution was to use these register you said me and to use 0x1D as direction register for the accelerometer-magnetometer.

      Thank you very much!

  5. When I run i2cdetect, I get the 1e, and the 6a address, but I also get UU at 3b. None of the tutorials can run for longer than a minute before I get: “failed to read block from I2C.” Any ideas?
    Thanks, Andy

      1. I did have another device connected to the gpio pins in the past. Not now though. I’ll try a clean install. Thanks.

      2. I black listed some audio drivers, and was able to free up all addresses on the i2c bus. The problem is still there. I can run all the tutorials, the data looks good for a while, but then the data freezes and I get: failed to read block from i2c. Not sure what to do next.

  6. Please I getting the “Failed to write byte to I2C Mag” error. I am using your code. The addresses on my I2C bus are 1d and 6b. Kindly advice.
    I am using the LSM9DSO and PI 3.

      1. Hi Mark,
        Thanks for the reply that works. Now I am trying to do the graphical display. How can I alter the code for the compass to just take up a fraction of my screen, say the top right corner.
        Thanks.
        Jeffrey A.

  7. Great stuff, would this sensor with a Pie connected to a marine NMEA network work as a replacement for a fluxgate compass to interface with a plotter with the ultimate goal of aiding the plotter in doing MARPA (RADAR target tracking) ? Marine fluxgate compasses cost more than $1000 so I’m thinking I can kill many birds with one stone building a Pie also capable of receiving AIS…

  8. I installed my GPS-EMUV3 and installed all the software for the Compass ( turorials 1 to 4) and it seeemd to work. But the compass needle stays all the time near the North. The heading is between about + 15.000 and 350.00. After starting tutorial1 the magRaw X values are about 750
    magRawY values are between +50 and -15
    magRawZ values are about 600
    It seems to me that the heading shoot bee from 0 until 360 degrees. What are I am doing wrong ?

  9. Yes, I did several times and it seems all oke. The heading results are between 15 degrees and 350 degrees (around the North), No results from 16 degrees to 349 degrees (around the South)
    Is it possible that the GPS-EMU V3 is not working properly?

      1. Results of tutorial 02:
        Heading 15.547 Compensated Heading 15,234
        Heading 351.796 Compensated Heading 350.957
        Tutorial 03
        magXmax 1023 magYmax 287 MagZmax -204 magXmin 955 magYmin 229 magZmin -298
        magXmax 638 magYmax -292 magZmax -574 magXmin 585
        magYmin -359 magZmin -649
        Tutorial 04
        Compensated Heading 333.984
        Compensated Heading 9.425

  10. I cannot make a screen shot but here a few lines of different positions of the EMU:
    heading 353.223 magRaw X 823 magRawY -60 MagRaw Z -763
    heading 12.953 magRaw X 656 magRawY 153MagRaw Z -752

  11. I’m consistently getting poor headings using your code, after calibration, trying several magnetometers with appropriate changes to register addresses: 2 different LSM9DS0s, a LSM303DLHC and a HMC5983. Rotating the sensor clockwise about the z axis in 90 degree steps, I get headings like these: 316, 273, 52, 271, 316, 273, 52, 271, 315, 272, etc. Thinking that my work bench environment might be magnetically distorted, I recalibrated in the middle of a football pitch and got the above headings, powering the RPi with a battery a meter away. My calibration technique is to rotate the sensor (and RPi) several times about each of the xyz axes running your calibration code.

    At this point I’m stumped. Please suggest where I might be going wrong. I can provide code and raw output if that would be helpful.

    1. I have a similar issue. I have followed the calibration guide and it seems that when the heading is 0 the Y axis is pointed roughly east. Is heading in the direction of the x axis?

  12. Hi Mark, i’m quite sure i’m done some mistakes with reference system, but can’t find the error. I implemented your code with Codesys 3.5 using an LsM9Ds1. I get heading correctly but tilt-compensation works fine only if PITCH<0 or ROLL<0, if not result is not compensated. Based on that have you idea where could be the problem ? I try to change randomly some plus and minus but same result.
    BR

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.