How to Make an NMEA-0183 Tilt-Compensated Compass and Rate-Of-Turn Indicator

This simple, analogue compass costs $10. If we want to use this for electronic chart-plotting applications, then we need an electronic compass that takes your magnetic heading and converts it into a digital signal. This compass does that, for the low, low price of $774.95. Why does an electronic compass, based on technology over four thousand years old, cost so much? This guide will show you how to build your own for under $100.

Recommended Gear

Now Why Is a Marine Compass so Expensive?

Aside from the obvious "it has the word 'marine' in the title, so it costs X times more," there's a valid reason it's more expensive than just a regular electronic compass. Any compass application for a moving vehicle needs to be securely mounted such that magnetic north aligns exactly with the vehicle longitudinal axis. In more basic terms, it needs to be completely flat and lined up with forward. When you take an analogue compass (like I linked to in the first paragraph) and tilt it just a little bit, the needle quickly jams on its pivot and it becomes unusable. So, in a rocking, rolling, listing sailboat, you need a tilt-compensated compass.

One way to do this is to put the compass on a two-axis gimbal (kind of like how some stoves are). This way, as the boat rolls and pitches, it remains flat relative to gravity, and will continue to work just fine. However, that requires you to build a two-axis gimbal, which isn't exactly a quick job if you want to do it correctly.

Fortunately, there's an easier way. Just get three compasses, and line them up so that one is flat against each axis. Lay one done so it's flat with respect to the horizon (Z), another with respect to the boat's longitude (X), and another with respect to the boat's latitude (Y). Then you just filter each compass's output through a complicated formula, and you get your magnetic heading. Fortunately, the MPU-9150 has taken care of all that hard work.

This guide assumes you've followed my previous post and set up the versatile MPU-9150 9-Axis Accelerometer, Gyro, and Compass. It should be fully working, and putting out valid data as demonstrated at the end of that post. If you have problems, comment at the bottom and I'll respond as soon as I can.

It also assumes you have gone through my other post and you have a good understanding of how to create an NMEA-0183 sentence with an Arduino. In fact, if you've gone through both of those posts and understand them, this is a very simple and quick job.

The Sketch for NMEA-0183 True Heading

 ////////////////////////////////////////////////////////////////////////////  
 //  
 // This file is part of RTIMULib-Arduino  
 //  
 // Copyright (c) 2014-2015, richards-tech  
 //  
 // Permission is hereby granted, free of charge, to any person obtaining a copy of   
 // this software and associated documentation files (the "Software"), to deal in   
 // the Software without restriction, including without limitation the rights to use,   
 // copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the   
 // Software, and to permit persons to whom the Software is furnished to do so,   
 // subject to the following conditions:  
 //  
 // The above copyright notice and this permission notice shall be included in all   
 // copies or substantial portions of the Software.  
 //  
 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,   
 // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A   
 // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT   
 // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION   
 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE   
 // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.  
 //  
 // Further modified by King Tide Sailing (kingtidesailing.blogspot.com)  
 //  
 ////////////////////////////////////////////////////////////////////////////  
   
 #include <Wire.h>  
 #include "I2Cdev.h"  
 #include "RTIMUSettings.h"  
 #include "RTIMU.h"  
 #include "RTFusionRTQF.h"   
 #include "CalLib.h"  
 #include <EEPROM.h>  
 #include <PString.h> 
   
 RTIMU *imu;               // the IMU object  
 RTFusionRTQF fusion;          // the fusion object  
 RTIMUSettings settings;         // the settings object  
  
 // User defined variables (for your own boat and geographic location):  
 float magd = -14.0;     // magnetic deviation; if it's East, it's negative (14E in San Francisco 2015)  
   
 // if you don't want to include a sentence, then just make the appropriate variable false  
 // it is recommended that you print only the minimum sentences desired to keep the Serial Port from getting too busy  
 boolean outputhdm = false; // magnetic heading  
 boolean outputhdt = true;  // true heading  
 boolean outputrot = true;  // rate of turn  
 boolean outputshr = true;  // pitch and roll  
   
 // Variables preset with recommended values (but can be changed):  
 #define DISPLAY_INTERVAL 500  // rate (in milliseconds) in which MPU9150 results are displayed  
   
 // Global variables:  
 unsigned long lastDisplay;  
 unsigned long lastRate;  
 int sampleCount;  
 float r = M_PI/180.0f; // degrees to radians  
 float d = 180.0f/M_PI; // radians to degrees  
   
 void setup()  
 {  
   int errcode;  
   
   Serial.begin(4800);   // output port to computer  
   Wire.begin();  
   imu = RTIMU::createIMU(&settings);  
   
   // Checking the IMU, if it fails, simply restarting the connection seems to work sometimes  
   if ((errcode = imu->IMUInit()) < 0) {  
     Serial.print("Failed to init IMU: "); Serial.println(errcode);  
   }  
   if (imu->getCalibrationValid())  
     Serial.println("Using compass calibration");  
   else  
     Serial.println("No valid compass calibration data");  
   
   lastDisplay = lastRate = millis();  
   sampleCount = 0;  
   
   // Slerp power controls the fusion and can be between 0 and 1  
   // 0 means that only gyros are used, 1 means that only accels/compass are used  
   // In-between gives the fusion mix. 0.02 recommended.  
     
   fusion.setSlerpPower(0.02);  
     
   // use of sensors in the fusion algorithm can be controlled here  
   // change any of these to false to disable that sensor  
     
   fusion.setGyroEnable(true);  
   fusion.setAccelEnable(true);  
   fusion.setCompassEnable(true);  
 }  
   
 // calculate checksum function (thanks to https://mechinations.wordpress.com)  
 byte checksum(char* str)   
 {  
   byte cs = 0;   
   for (unsigned int n = 1; n < strlen(str) - 1; n++)   
   {  
     cs ^= str[n];  
   }  
   return cs;  
 }  
   
 void loop()  
 {   
   unsigned long now = millis();  
   unsigned long delta;  
   int loopCount = 1;  
    
   while (imu->IMURead()) {  
     if (++loopCount >= 10)  
       continue;  
     fusion.newIMUData(imu->getGyro(), imu->getAccel(), imu->getCompass(), imu->getTimestamp());  
     sampleCount++;  
     if ((delta = now - lastRate) >= 1000) {      
       sampleCount = 0;  
       lastRate = now;  
     }  
     if ((now - lastDisplay) >= DISPLAY_INTERVAL) {  
       lastDisplay = now;  
       RTVector3 pose = fusion.getFusionPose();  
       RTVector3 gyro = imu->getGyro();  
       float roll = pose.y()*-1*d;     // negative is left roll  
       float pitch = pose.x()*d;       // negative is nose down  
       float yaw = pose.z()*d;         // negative is to the left of 270 magnetic  
       float rot = gyro.z()*d;         // negative is to left  
       float hdm = yaw-90;             // 0 yaw = 270 magnetic; converts to mag degrees  
       if (yaw < 90 && yaw >= -179.99) {  
        hdm = yaw+270;  
       }  
       float hdt = hdm-magd;           // calculate true heading  
       if (hdt > 360) {  
         hdt = hdt-360;  
       }  
       if (hdt < 0.0) {  
         hdt = hdt+360;  
       }  
   
       // HDM Magnetic Heading //  
       if (outputhdm = true){  
       char hdmSentence [23];   
       byte csm;  
       PString strm(hdmSentence, sizeof(hdmSentence));  
       strm.print("$HCHDM,");  
       strm.print(lround(hdm));  // lround simply rounds out the decimal, since a single degree is fine enough of a resolution
       strm.print(",M*");   
       csm = checksum(hdmSentence);  
       if (csm < 0x10) strm.print('0');  
       strm.print(csm, HEX);  
       Serial.println(hdmSentence);  
       }  
   
       if (outputhdt = true){  
       // HDT True Heading //  
       char hdtSentence [23];   
       byte cst;  
       PString strt(hdtSentence, sizeof(hdtSentence));  
       strt.print("$HCHDT,");  
       strt.print(lround(hdt));  
       strt.print(",T*");   
       cst = checksum(hdtSentence);  
       if (cst < 0x10) strt.print('0');  
       strt.print(cst, HEX);  
       Serial.println(hdtSentence);  
       }  
   
       if (outputrot = true){  
       // ROT Rate of Turn //  
       char rotSentence [18];   
       byte csr;  
       PString strr(rotSentence, sizeof(rotSentence));  
       strr.print("$TIROT,");  
       strr.print(lround(rot)*60);    // multiply by 60, since ROT is measured in degrees per minute
       strr.print(",A*");   
       csr = checksum(rotSentence);  
       if (csr < 0x10) strr.print('0');  
       strr.print(csr, HEX);  
       Serial.println(rotSentence);  
       }  
   
       if (outputshr = true){  
       // SHR Pitch and Roll (no heave... yet...)//  
       char shrSentence [50];   
       byte csp;  
       PString strp(shrSentence, sizeof(shrSentence));  
       strp.print("$INSHR,,");  
       strp.print(lround(hdt));  
       strp.print(",T,");  
       strp.print(lround(roll));  
       strp.print(",");  
       strp.print(lround(pitch));  
       strp.print(",,,,,1,0*");  
       csp = checksum(shrSentence);  
       if (csp < 0x10) strp.print('0');  
       strp.print(csp, HEX);  
       Serial.println(shrSentence);  
       }  
      }  
    }  
 }  

So what's going on here? As long as you mount the MPU-9150 completely flat with respect to the boat (parallel with the water line, forward pointing towards the bow), then it will give you valid heading data. You must adjust a few variables at the top, mainly the magnetic deviation for where you're boat is. If you're having trouble finding that, it may help to look up nearby airports, since magnetic deviation is important for aviation.

The variables just below that dictate what gets printed. For the most part, marine applications use true heading (as do aviation applications), but if your chart plotter incorporates a geodetic magnetic deviation model, then you may just want to print the magnetic sentence.

I also have the lround function in there, which gets rid of the decimal place for the headings. A single degree is fine enough of a resolution (101.5 degrees means the same to me as 102 degrees). If you wish to keep the decimals, then just remove the lround functions.

The HDM and HDT sentences are self-explanatory enough, but what about ROT? Simple: it's Rate of Turn, how many degrees per second your boat is turning. And SHR? That's the roll and pitch of the boat, which you can end up doing some pretty cool things with if you know how to use them.

That's really all there is to say about this. It's a simple sketch, and it's with some pretty low cost parts. In fact, if you tally it up, you get a tilt-compensated compass for about ~$100. Now that's more like 2015.

Comments

  1. I think you forgot one important detail that will make people far removed from knowledge of NMEA0183 and ARDUINO long to suffer in order to understand why, for example, MFD Raymarine or Garmin do not see the data from the compass.
    If you need to connect the compass to the factory MFD, you need to convert TTL to RS232. This can be used, for example, interfaces based MAX3232 converter chip.

    ReplyDelete
  2. And one more
    "if (outputhdm = true)" - not working
    Need to replace to "if (outputhdm)"
    Because it is alredy boolean.

    ReplyDelete
    Replies
    1. To be honest it doesn't work as (outputshr = true) is always true as it's an assignment and not a comparison :) it should have been written as (outputshr == true) which is equivalent to (outputshr) as test condition

      Delete
    2. The same consideration about comparisons and assignments apply to all the other tests

      Delete
  3. And more
    Where in this libraries "tilt compensation" calculate?
    i was try to use your scatch with mpu9120 and another scatch with GY-85 with next compensation procedure.
    ///////////////////
    // Tilt compensation
    float tiltCompensate(Vector mag, Vector normAccel)
    {
    // Pitch & Roll
    float pitch;
    float roll;
    pitch = asin(-normAccel.XAxis);
    roll = asin(normAccel.YAxis);
    if (roll > 0.78 || roll < -0.78 || pitch > 0.78 || pitch < -0.78)
    {
    return -1000;
    }
    // Some of these are used twice, so rather than computing them twice in the algorithem we precompute them before hand.
    float cosRoll = cos(roll);
    float sinRoll = sin(roll);
    float cosPitch = cos(pitch);
    float sinPitch = sin(pitch);
    //Compensation
    float Xh = mag.XAxis * cosPitch + mag.ZAxis * sinPitch;
    float Yh = mag.XAxis * sinRoll * sinPitch + mag.YAxis * cosRoll - mag.ZAxis * sinRoll * cosPitch;
    float heading = atan2(Yh, Xh);
    return heading;
    }
    ///////////////////////
    IMHO - last scatch realy better compensate roll and pitch moving of compass.

    ReplyDelete
  4. have you test this in real conditions, is this accurate?

    ReplyDelete
    Replies
    1. On the Arduino, it's a "maybe." I found it was inconsistent and swung +/- 15 degrees, but I suspect that was a bad chip due my sloppy soldering and weak connectors.

      On the Raspberry Pi, however, it's rock solid. No weird deviations, always accurate, and extremely reliable. Testing conditions were in the San Francisco Bay, spinning in 360s, tacking, jibing, solid rocking and rolling, during the summer wind blasts. The reason is that you can get much better calibration on the Raspi instead of the Arduino.

      See my write up for how to do this: https://kingtidesailing.blogspot.com/2016/02/how-to-setup-mpu-9250-on-raspberry-pi_25.html

      Delete
  5. Thanks for sharing.. im getting good results too. Your blog is great

    ReplyDelete
  6. One more technical question, how the rate of turn is related to the speed? for examples my speed is 3kn and the rate of turn is for example 15degree/s, what will be the rate of turn if i travel with 20kn? is there any mathematical formula that defines this? thanks

    ReplyDelete
    Replies
    1. Rate of Turn (ROT) is equal to the Velocity (V) divided by the turning Radius (R), which is the center of the circle that the boat is following by turning. Thus:

      ROT = V/R

      So if you are sailing along (or motoring, I suppose), and double your speed (V) without touching the tiller (which affects your rate of turn), your turning radius will also double. If you double your speed, but let's say you want to keep the same turning radius, then you will have to push the tiller over even further, which will increase your ROT.

      Delete
    2. So for your example:

      15 degrees per second is 900 degrees per minute (that's how the above formula is measured).

      900 = 3 knots / R

      which gives us a turning radius of 0.0033 nautical miles (20 feet).

      If you keep the same turning radius, and increase your speed to 20 knots:

      ROT = 20 / .0033 = 6000 degrees per minute (100 degrees per second).

      If you keep the same rate of turn and increase your speed to 20 knots:

      900 = 20 / R

      R = .0222 nautical miles, or 135 feet.

      Delete
    3. Here's a good write up about all that:

      https://www.slideshare.net/DheerajKaushal1/91121543-roti-1

      Delete
  7. Great explanation. Thanks a lot.

    ReplyDelete

Post a Comment

Popular Posts