Saturday, June 23, 2012

Compass calibration on the water

Even if an electronic compass has been meticulously calibrated on land in a non-disturbed magnetic environment, it will need additional calibration adjustments once installed in a boat.

In a previous post, I described a procedure to recalibrate a compass with a GPS if you can find a patch of water where there is absolutely no current, a situation that practically does not exist in my maritime environment.

If your system can log the headings from a fast compass (5 Hz or more), here is an alternate method that can tolerate an existing current, but requires absolutely flat water (no waves) and no (or very negligible) wind. This is in fact the method that many commercial compass vendors choose to implement in their autocalibration routines, but presented here in a transparent manner.

These conditions must be met: a low and constant motoring speed (2 to 4 knots), and a tiller or wheel blocked in position so that a complete turn will take around 3 minutes.

Here is how I produced the results presented at the end of this post.

I made several turns while logging the headings (for both the Aimar H2183 and my own Hi-Resolution compass), as well as the boat speed. From the speed log, I find a section where the boat speed has been very stable, and I extract the heading data from a full turn.

We can make the reasonable hypothesis (with no waves and no wind) that our angular velocity has been constant during this golden turn, so that all recorded headings have to be separated by equal angles. If this is not the case, we can calculate the local deviation.

So several turns are recorded, but only the best one is used in the calibration. Here is simplified example using only a reduced set of measurements.




In Colum A, we have the measured headings on a complete turn, with 20 diffferent measurements. The expected angle difference between each measurement is 360/20 = 18 degrees. In Column B, we have calculated these expected headings if there was no deviation. In Column C, we calculate the deviation (A – B).

We can graph these deviations (C) vs. the measured heading (A). This is our deviation graph.



From these data points, we can calculate (using the NLREG software)  the 5 deviation coefficients (A, B, C, D, E) such as that:

Deviation = A + B sin(H) + C cos(H) + D sin(2*H) + E cos(2*H).

By correcting the measured heading by the calculated deviation, we obtain a pre-calibrated heading that still needs a last offset correction.  Our graph is based on the assumption that there is no deviation at our starting heading of 100 deg. But this is not necessarily the case; the zero-deviation point(s) may be at some other heading(s).

This last correction is easy to do, as we need only to compare any pre-calibrated heading value to a known heading. For example if, with the boat aligned along a dock at 78.0 deg, we obtain a pre-calculated heading of 76.5 deg, we will have to add a constant offset of 1.5 deg to all our pre-calibrated headings. (Don't trust your regular magnetic compass for this : it has its own uncorrected deviations).

Final corrected heading  =  Measured heading  - Deviation + Offset


Here are the deviation curves obtained from my 2 compass, using data from the same turn.


Airmar H2183 Compass  (1721 meassurements, doubled 5 Hz samples ):






Hi-Resolution Compass (1737 measurements, 10 Hz samples):




So are we done?

Not really, because be still have to check if this calibration remains valid:
 - when the motor is not running
 - when the boat is heeling.


Getting an accurate magnetic heading is a never ending story.

Thursday, June 21, 2012

Interfacing the Raymarine ST1000+ Autopilot

I understand that the Raymarine ST1000+ autopilot (and the similar ST2000+) can receive exterior heading data through the SeaTalk bus, specifically from a Raymarine ST40 fluxgate compass. So another project would be to use the heading output from my hi-resolution compass to drive the autopilot.

First I was interested to see what kind of data the autopilot puts on the SeaTalk bus as a talker. I used the following arrangement to monitor the SeaTalk bus.



The autopilot is connected to a serial port of a MAVRIC-IIB board through the SeaTalk interface circuit. The MAVRIC-IIB has an onboard MAX222 chip to provide level conversion between the serial port and the logic level pins of the ATMega128 microcontroller. The SeaTalk datagrams showing on the bus are resent in hex format to a computer running Hyperterminal, through a serial-to-USB converter.

The microcontroller code is an almost direct port of the “SeaTalk Monitor” code available on Thomas Knauf’s website. It can be found at the end of this post. In normal operation (standby or auto), the ST-1000+ sends continuously these 2 datagrams, each one at 1 Hz, with a half-second between them.


9C C1 07 00
84 C6 07 00 00 00 00 00 0F
9C C1 07 00
84 C6 07 00 00 00 00 00 0F
9C C1 07 00
84 C6 07 00 00 00 00 00 0F
9C C1 07 00
84 C6 07 00 00 00 00 00 0F


Here, the 9C datagram encodes the compass heading (in degrees) and the turning direction (left or right).

The 84 datagram also encodes the compass heading and turning direction, plus the autopilot course when in auto mode, and the currently active mode (standby, auto, vane or track mode).

We learn on Thomas Knauf’s site that the 89 datagram encodes the compass heading sent by the ST40 compass instrument, and can be read by the ST1000+ or ST2000+ autopilot. To test this behaviour, I modified the microcontroller code (not shown here) so that it sends an 89 datagram (89 02 04 00 20) after reading each of the 9C and 84 datagram, with a small delay to avoid collisions. The datagram sent (89 02 04 00 20) encodes a heading of 8 degrees.

The monitor then produced this kind of ouput:


9C 01 04 00
89 02 04 00 20
84 06 04 00 00 00 00 00 0F
89 02 04 00 20
9C 01 04 00
89 02 04 00 20
84 06 04 00 00 00 00 00 0F
89 02 04 00 20


From what I observed by playing with the autopilot in standby and auto mode, the 89 datagram is interpreted by the ST-1000+ as a request to perform a heading alignment to the value encoded in the datagram.

So if a custom heading is encoded in an 89 datagram sent on a regular basis, the autopilot will indeed adjusts its heading alignment if required to reproduce this value.


#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdio.h>

unsigned char resh;
char buffer[256];
char hex[]="0123456789ABCDEF";

unsigned int collision_ctr;
unsigned char in_ptr, out_ptr, limit_ptr;
char receiver_buf, byte_ctr;


unsigned char kbhit(void)
{
 // return nonzero if char waiting
 unsigned char b;
 b = 0;
 if(UCSR1A & (1 << RXC1))
  b = 1;
 return b;
}


int uart_putchar0(char c)
{
 loop_until_bit_is_set(UCSR0A, UDRE0);
 UDR0 = c;
 return 0;
}


int main(void)
{
  in_ptr = 0;
  out_ptr = 0;
  limit_ptr = 0;

  /* enable serial port UART1 */  /* Set baud rate : 4800 @ 16 MHz */
  UBRR1L = (unsigned char)(207);
  /* Enable receiver with 9 data bits  */
  UCSR1B = _BV(RXEN1) | _BV(UCSZ12);

  /* enable serial port UART0 */  /* Set baud rate : 9600 @ 16 MHz */
  UBRR0L = (unsigned char)(103);
  /* Enable transmitter  */
  UCSR0B =_BV(TXEN0);

  for(;;)
  {
    if(kbhit())
    {
      // check 9th bit before reading UDR1
      resh = UCSR1B;
      resh = (resh >> 1) & 0x01;

      receiver_buf = UDR1;
 
      if(resh)     //  9th bit set
      {
        if(byte_ctr)   // More characters expected => Collision
        {
          in_ptr = limit_ptr;    // Discard last datagram, restart from beginning 
          collision_ctr++;     // Count collision events 
        }
        buffer[in_ptr++] = '\r';  // Put new command on new line 
        buffer[in_ptr++] = '\n';
        byte_ctr = 255;  // Undefined datagram length, wait for next character
      }
      else
      {
        if(byte_ctr == 254)       // Attribute byte ? 
        byte_ctr = (receiver_buf & 0xF) + 2; // Read expected datagram length
      }

      if(byte_ctr)
      {            // Process valid data bytes, should always be true 
        buffer[in_ptr++] = hex[receiver_buf >>  4]; // Convert Data to hex 
        buffer[in_ptr++] = hex[receiver_buf & 0xF];
        buffer[in_ptr++] = ' ';                     // Seperate by space 
        if(!--byte_ctr)
          limit_ptr = in_ptr;        // Complete datagram ready for output
      }
    }
    else
    {
      if(out_ptr != limit_ptr)          // Characters waiting for Output ?
      {
        // Copy single character from buffer to screen
        uart_putchar0(buffer[out_ptr++]);
      }
    }
  }
  return 1;
}