Projects

Getting Your Bearings

Arduino Based Magnetic Compass

Andy Clark

Issue 19, February 2019

This article includes additional downloadable resources.
Please log in to access.

Log in

Instead of pointing to a cold spot at the top of the globe, this compass points you to your heart’s desire. In other words, pointing to where you want to go!

Rather than pointing North, this project started out as a modern take on the South Pointing Chariot. That was a wheeled chariot with a figure on the top. A complex arrangement of differential gears ensured the figure always pointed in the same direction.

My thought was this could be done with electronics. I also thought, why just point South? Why not point to where you want to go?

I quickly realised that I’d reinvented the Captain Jack’s Compass from the Pirates of the Caribbean films. One of the things I like about his compass is the simplicity of the interaction. You just hold it and it points to your heart’s desire.

Now, reading your 'heart’s desire' is a bit beyond even today’s electronics obviously. I needed some way to capture the target location and did not want to add a screen with controls to the device. The obvious choice was to have some form of web front end to set the coordinates. The combination of a handheld device and the need for WiFi led me to select the Arduino MKR1000 board for the main controller.

THE BROAD OVERVIEW

To be able to point to a destination you need to know where you are and what direction your device is pointing. We can get both of these with a GPS and Compass module designed for drones. These are reasonably sophisticated electronic sensors but their outputs are easily interpreted using a microcontroller.

To make a pointer turn 360° we can use a small stepper motor. To know the starting point of the motor, an optical sensor is used.

breakdown

HOW IT WORKS

When the device starts up, it rotates the pointer using the stepper motor until a signal is generated from the reflective homing sensor. From this point, it knows the position of the pointer relative to its starting point.

When the user connects to the web address of the device they are presented with a simple HTML form, which is where they enter the desired latitude and longitude. Submitting the form passes these parameters back to the Arduino via a GET method. The parameters are parsed by the Arduino and the values stored.

A GPS module continually sends information over a serial connection to the Arduino. This is filtered and parsed by the Arduino to extract the current position which is also stored.

Periodically the Arduino requests the orientation of the device from the magnetic sensor module.

The compass uses these three pieces of information, target, current location and orientation to calculate the new position of the pointer.

The pointer is then turned using the stepper motor to the desired orientation.

As the device moves, the current position and orientation are updated. The pointer will continue to adjust to the target location accordingly.

Prototyping

Having used stepper motors with microcontrollers, I knew that I’d need some form of driver circuit to energise the motor coils. I’ve used Darlington drivers for this before but not from a 3.3V device like the MKR board.

I also knew I’d need to have some form of sensor to allow the motor position to be determined. I had a small reflective sensor from a prior project and thought that could work. So to test the motor and sensor I made a simple test rig.

prototype

The reflective sensor uses an infrared LED and a photodiode side by side in the same package. When a reflective item is in front of the sensor the diode turns on. In this sensor, the output is already buffered with a logic gate. The advantage of this approach over a slotted photo interrupter is that we can place the sensor to one side of the motor, rather than needing it up be above the output shaft.

The GPS module constantly streams messages over a serial connection. To understand this better, I cut off the default connectors and soldered on 0.1” sockets that could be used with headers. This allowed me to hook the module to a USB to TTL serial adapter plugged into my PC. The module manufacturer provides a U-Centre tool to analyse the data coming from the module. Note that this tool should work with other modules as long as they use the NMEA standard (National Marine Electronics Association).

module

The module was not configured for 9600 baud as suggested by several sites, but 38400. So the initial data received was garbled. I swapped over the baud rate and reviewed the characteristic $Gxxxx messages expected from the device. One thing to watch for is that many of the code examples for this device are US centric so their signals always start with $GP whereas other parts of the globe might see $GL, $GA, $GB or $GN where the second character represents the satellite network that is providing the location, GN is generic. You may need to adjust the code examples to ignore that second letter.

I had initially wondered why the board was not producing any direction information. I then realised that this was provided by a separate chip mounted in the module.

The digital compass is provided by a HMC5883L chip. This uses an I2C protocol so a separate connection to the microcontroller is required. The signals used by the I2C protocol work by having devices leave the line floating or pulling it to ground. This allows the master and slave to use the same data line with the data transferring in two directions. This also means we need 2 resistors to pull the lines high. The manufacturer’s recommendation is to use 2k2 resistors but you may need to make these smaller if you want to have a long cable run or want to run the module at higher speeds. You can also increase this value to save power, a value of 10k is suggested for I2C devices running at 100kHz (standard speed) is suggested. I ran a simple example to confirm I was receiving data from the compass. If you want to complete the same test yourself with an Arduino, details of the wiring and example code can be found on the Adafruit website: https://learn.adafruit.com/adafruit-hmc5883l-breakout-triple-axis-magnetometer-compass-sensor/wiring-and-test

Arduino

The MKR1000 board is compatible with the Arduino WiFi Shield 101, so the examples from that works with this board. I adapted one that acted as a web server and tested that I could receive coordinates and echo them to the serial port.

The Build:

ASSEMBLY

parts
Parts Required:JaycarAltronicsCore Electronics
1 × Arduino MKR1000--DEV-14394
1 × U-blox NEO-M8N GPS Module--TEL0101
1 × 68Ω 1/4W Resistor*RR0544R8032-
2 × 2k2Ω 1/4W Resistors*RR0580R7566-
1 × 28BYJ-48 Stepper Motor with ULN2003 Control BoardXC4458Z6330-
1 × ULN2003 Darlington ArrayZK8855Z3000003-ULN2003A
1 × Omron EE-SY310 Reflective Opto sensorSee note belowSee note belowSee note below
1 × 3.7V LiPo Battery with a female 2 pin JST PHR2 Connector--CE04377
PerfboardHP9552-ADA2670
USB Socket--ADA3258
Header StripHM3212P5430POLOLU-965
See note below

Parts Required:

1 × Arduino MKR1000-
1 × U-blox NEO-M8N GPS Module-
1 × 68Ω 1/4W Resistor*RR0544
2 × 2k2Ω 1/4W Resistors*RR0580
1 × 28BYJ-48 Stepper Motor with ULN2003 Control BoardXC4458
1 × ULN2003 Darlington ArrayZK8855
1 × Omron EE-SY310 Reflective Opto sensorSee note below
1 × 3.7V LiPo Battery with a female 2 pin JST PHR2 Connector-
PerfboardHP9552
USB Socket-
Header StripHM3212

*Quantity shown, may be sold in packs. Breadboard, prototyping hardware, and hookup wire are also required.

Note:

  1. If the NEO-M8N is difficult to source then it is possible to purchase a separate GPS module and HMC5883L on a breakout board.
  2. Our local retailers do not appear to stock the Omron Opto sensor. You will need to source this from Mouser (part. 653-EE-SY310) or Digikey (Part. OR525-ND).

I used a small single perfboard to provide a common place for connecting the components. Sockets were used for the Arduino with headers for the Neo8M connectors, motor and optical sensor. Thin wires were used for the signal with slightly thickers ones used for the motor power and ground. The 2k2 pull up resistors for the SDA and SCL lines were fitted between the sockets.

Before plugging in the Arduino, the wiring was checked for shorts and open connections using a multimeter.

The Neo8M had come with connectors to attach it to a Drone controller. I swapped these with 0.1" sockets which fitted onto my headers.

board 3

The optical sensor was fitted to a small piece of perfboard with the 68 Ohm resistor in series with the IR LED. This connected up to the headers on the main board. The sensor was tested with a simple piece of code that turned on an LED when the sensor was activated.

opto sensor

When I purchased the stepper motor it came with a small breakout board with LEDs. There are smaller surface mount versions of this board or you can simply wire the Darlington Driver on it's own breakout board.

For the stepper motor to work correctly, you need to energise the coils in the right order, so check the cable colours against the data sheet for your motor. I used the code from the prototyping stage to ensure each of the items were hooked up correctly.

THE CODE

The MKR series of boards require you to install the relevant driver and core libraries. You should find that if you start the Arduino IDE, and then plug in the board, it will take you to the correct place to install these. Otherwise, open the Tools->Board->Board manager menu and search for MKR. Install the “Arduino SAMD Boards” package.

The software to run the compass is written in C++ and takes advantage of the libraries written for Arduino and the sensors.

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_HMC5883_U.h>
#include "TinyGPS++.h"          
#include "CustomStepper.h"

The Wire library provides the connectivity to the I2C bus used by the magnetic compass. The compass data is accessible via the Adafruit universal driver, which is a standardised approach to reading sensors. This approach means that once you know how to use one type of sensor you can use any. It also means you can swap out a sensor with a different one that performs the same function, and just swap in a new driver rather than having to rewrite the code.

The HCM5883 library can be installed from the Manage Libraries menu in the Arduino IDE The others must be copied locally. https://learn.adafruit.com/using-the-adafruit-unified-sensor-driver/introduction

The GPS module uses a serial connection, which the Arduino framework has built in. The TinyGPS++ library from Mikal Hart is used to decode the data from this module. https://github.com/mikalhart/TinyGPSPlus

For the stepper motor, we use a customised version of the CustomStepper library by Igor Campos. The key feature of this code is that it allows us to move the motor whilst doing other activities on the board. Additional functions have been added to get the current position and to home the motor. This was originally written for my Topsy Turvy clock project where each of the hands of the clock is driven independently by stepper motors. https://github.com/Workshopshed/TopsyTurvyClock

The final libraries are for the WiFi, and allows our board to act as a web server. The Wifi101 library needs installing from the Manage Libraries menu.

#include <WiFi101.h>
#include "arduino_secrets.h"

The arduino_secrets.h file holds WiFi SSID and passkey. Set these to match your WiFi network.

#define SECRET_SSID "MyWifi"
#define SECRET_PASS "MyWifiPass"

At this point, the code declares some variables to represent our input and output devices. The HMC5883 takes a unique identifier, the custom stepper needs to provide the pin details, stepping sequence, steps per revolution, speed and direction. TinyGPSPlus does not require parameter.

Adafruit_HMC5883_Unified mag = Adafruit_
HMC5883_Unified(12345);
CustomStepper stepper(D3,D2,D1,D0,D5,D4, 
(byte[]){8, B1000, B1100, B0100, B0110, B0010, B0011, B0001, B1001}, 4075.7728395, 10, CW);
TinyGPSPlus gps;

The WebServer is also initialised listening on web port, port 80.

char ssid[] = SECRET_SSID;   
char pass[] = SECRET_PASS;  
int keyIndex = 0;               
int WifiStatus = WL_IDLE_STATUS;
WiFiServer server(80);

In the setup code, we configure the devices.

Serial1.begin(38400);

The setup for the GPS is a case of turning on the serial port at the right speed. The Arduino MKR 1000 has two serial ports. One is connected back to the PC on the USB connector. The other SERIAL1 is connected to pins D13 (RX) and D14 (TX). Note that some other boards don’t have the second port but you can emulate the hardware using a software serial port instead.

To initialise the stepper we need to move it around until we toggle the optical sensor from off to on. The position is zeroed at this point.

stepper.home();

There are two steps to configure the webserver. First, we connect to the WiFi network and then we start the webserver.

//attempt to connect to WiFi network:
while ( WifiStatus != WL_CONNECTED) {
  Serial.print("Attempting to connect to Network named: ");
  Serial.println(ssid);      // print the network name (SSID);
  // Connect to WPA/WPA2 network. Change this line if using open or WEP network:
  WifiStatus = WiFi.begin(ssid, pass);
  delay(10000); //wait 10 seconds for connection: 
}
server.begin();        //start the web server on port 80
printWiFiStatus();      //you’re connected now, so print out the status

A function deals with receiving the incoming web requests. It sends the form to enter the parameters and parses the submitted values to pass to the code. Note this is a simple parser, so the case and order of the parameters passed are important.

void webRequest() {
  WiFiClient client = server.available();  // listen for incoming clients
  if (client) {        // if you get a client,
    Serial.println("new client");  // print a message out the serial port
    String currentLine = "";    // make a String to hold incoming data from the client
    while (client.connected()) {  // loop while the client’s connected
      if (client.available()) {  // if there’s bytes to read from the client,
        char c = client.read();  // read a byte, then
        //Serial.write(c);    // print it out the serial monitor uncomment for debugging
        if (c == ‘n’) {    // if the byte is a newline character
          // if the current line is blank, you got two newline characters in a row.
          // that’s the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) {
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
            // and a content-type so the client knows what’s coming, then a blank line:
            client.println("HTTP/1.1 200 OK"); client.println("Content-type:text/html");
            client.println();
            // the content of the HTTP response follows the header:
            client.print("<html><body><h2>Magic Compass</h2>");
            client.print("<p><form action="/SetLoc" method="get">");
            client.print("<div><label for="lon">Longitude:</label><input name="lon" 
id="lon"></div>");
            client.print("<div><label for="lat">Latitude:</label><input name="lat" 
id="lat"></div>");
            client.print("<div><button type="submit">Send</button></div></form></p>");
            client.print("</body></html>");
            // The HTTP response ends with another blank line:
            client.println();
            // break out of the while loop:
            break;
          }
          else {      
            // Parse a valid input GET /SetLoc?lon=10&lat=20
            if (currentLine.startsWith("GET /SetLoc?")) {
              int pos = currentLine.indexOf("?");
              if (pos != -1) {
                Serial.println("New destination");
                int posEnd = currentLine.indexOf(" ",pos);
                Serial.println(currentLine.substring(pos,posEnd));
                int posLon = currentLine.indexOf("lon=",pos+1);
                int posLonEnd = currentLine.indexOf("&",posLon+1);
                targetLongitude = currentLine.substring(posLon+4,posLonEnd).toDouble();
                int posLat = currentLine.indexOf("lat=",posLonEnd+1);
                int posLatEnd = currentLine.indexOf(" ",posLat+1);
                Latitude = currentLine.substring(posLat+4,posLatEnd).toDouble();
                Serial.println(Longitude,5);
                Serial.println(Latitude,5);
              }
            }
            // reset for next line:
            currentLine = "";
          }
        }
        else if (c != ‘r’){    // if you got anything else but a carriage return character,
          currentLine += c;    // add it to the end of the currentLine
        }
      }
    }
    // close the connection:
    client.stop();
    Serial.println("client disconnected");
  }
}

The main loop is responsible for reading the sensors and calculating the direction. First, we check if anything has been sent to the web interface using the previous function. Then we capture the current orientation from the compass.

webRequest();
if(mag.begin()) {
  getCompassInfo();
}

The compass is read by creating an event object and requesting an event from the driver. We also need to adjust the magnetic compensation. This is needed as the magnetic sensor does not actually point to the magnetic North, but instead, aligns itself to the local magnetic field. These fields are not regular and have curves and distortions. Some GPS modules provide the local magnetic deviation in the RMC message but my messages didn’t have a value in that field, hence I hardcoded the value. Note that values to the East are positive and to the West negative.

LocationMagnetic DeclinationMagnetic Declination Decimal
Darwin +2° 35’ 2.5833
Brisbane +10° 59’ 10.9833
Sydney +12° 41’ 12.6833
Melbourne +11° 38’ 11.6333
Adelaide +7° 57’ 7.95
Perth -1° 43’ -1.7166
Alice Springs +4° 33’ 4.55
Hobart +15° 5' 15.0833
Canberra +12° 25' 12.4166
Wellington +22° 38' 22.6333
Christchurch +23° 59' 23.9833
magnetic paths
void getCompassInfo(){
  /* Get a new sensor event */ 
  sensors_event_t event; 
  mag.getEvent(&event);
  float heading = atan2(event.magnetic.y,
event.magnetic.x);
  float declinationAngle = 0.05;
  heading += declinationAngle;
  // Correct for when signs are reversed.
  if(heading < 0)
    heading += 2*PI;
  // Check for wrap due to addition 
of declination.
  if(heading > 2*PI)
    heading -= 2*PI;
  // Convert radians to degrees
orientation = heading * 180/M_PI; 
}

Next is to read the data from the Serial1 port connected to the GPS. This is passed to the TinyGPS++ encode function to tell us if there is a new reading.

if (Serial1.available() > 0) {
  if (gps.encode(Serial1.read())) {
    if (gps.location.isValid()) {
      Latitude = gps.location.lat();
      Longitude = gps.location.lng();
    }
  }
}

Once we have the three pieces of information; orientation, location, and target, we can determine the new bearing. The calculation of the bearing is handled by the TinyGPS++ library. This uses a simplified model of the globe, which although is an approximation, is good enough for this use.

double courseToTarget =
  TinyGPSPlus::courseTo(
  Latitude,
  Longitude,
  targetLatitude, 
  targetLongitude);

The new bearing is passed to the stepper library, adjusted for the current position. For each loop, we call the Run function on the stepper motor, which determines if it is time to move and how many steps to move.

stepper.rotateToDegrees(courseToTarget-
orientation);
stepper.run();

DESIGNING THE CASE

I wanted the case to be that same octagonal shape of Captain Jack’s version, but I thought that the design could be a bit more interesting. The first step in the design was to roughly model each of the key components in CAD. That allowed me to find a layout that fits all the components into this awkward shape.

the case

It is important when designing the case that the motor spindle is in the centre, so that was the first design decision. I also wanted the motor supports to be strong, so these were made extra chunky with big fillets around their base.

Another design issue was ensuring that the GPS signal was not blocked by other components. This was done by mounting the GPS in the top of the case.

I wanted to be able to charge the battery or reprogram the Arduino without disassembly. For this, I added a panel mounted USB connector which meant I could place the Arduino further into the case.

connector

To give my case an older appearance, I chose to make some of the pieces out of plywood. I have included a template if you want to make your own timber pieces. You can download this from the Resources section on our website, along with 3D print files if that is what you prefer.

To create the wooden top, a template was printed out and stuck to some 3mm plywood. This was cut oversize with a fine toothed saw and then sanded to size using the base as a reference.

The inner hole was cut by drilling a hole then using a coping saw. The inner hole does not need to be too accurate as is covered by the indicator ring and dome. The template was used to drill the two mounting holes. The side panels were cut from a strip of plywood packing material and sanded for a tight fit into the sides.

Both the top and sides were stained with a dark oak stain. I was lucky to have a 65mm disk left over from a Doll's House kit. I marked the centre with the help of a centre finder and drilled is steps to 10mm.

compass disk

The compass background is available for download from the digital resources on our website. Print this out and glue it to the compass disk.

USING THE COMPASS

When you first power on the compass it needs to be in range of your WiFi. Use your phone or PC to navigate to the web page and submit a new latitude and longitude setting.

Once these are set it is safe to leave the network as long as you don’t disconnect the power.

You may need to move outside to get a GPS fix. Once a fix is established the pointer should rotate to point to where you want to go.

compass face

WHERE TO FROM HERE?

A simple addition to this project would be an SD Card to store the previously selected coordinates, and a source HTML for a more sophisticated input form.

The compass does not need to track a static item. You could use GPS data from a phone or other device to constantly update your compass via its web API.

To reduce cost, you could use a simpler Arduino board and hard code your target values rather than setting them via the WiFi. Alternatively, you could use the serial port from a PC to set the values.

If you created two devices you could have them always pointing at each other. Or perhaps have a second pointer to track multiple targets?

If you add a second stepper motor you could have an outdoor robot that could move to a point of your choosing.

compass exploded