Wednesday, April 27, 2011

Tilt compensation code

This is the microcontroller code for the basic tilt-compensated compass described in the previous post. This will send the following results through the serial port at a baud rate of 9600: uncorrected heading, corrected heading, heel angle, pitch angle.

/* Basic tilt-compensated compass
 * Micromag3 magnetometer and SCA3000 accelerometer
 * using the WaveShare STK128+ Standard development
 * board with voltage level jumper set to 3.3 V


 * SCA3000 MOSI -> PB2(MOSI)
 * SCA3000 MISO -> PB3(MISO)
 * SCA3000 SCK -> PB1(SCK)
 * SCA3000 CSB -> PB4
 * SCA3000 RST -> PB5
 * SCA3000 INT (not connected)
 * SCA3000 VIN -> 5 V from USB port
 * SCA3000 GND -> common ground


 * Micromag3 MOSI -> PB2(MOSI)
 * Micromag3 MISO -> PB3(MISO)
 * Micromag3 SCLK -> PB1(SCK)
 * Micromag3 SSNOT -> PE2
 * Micromag3 RESET -> PB6
 * Micromag3 DRDY -> PE5
 * Micromag3 VDD -> 3.3 V
 * Micromag3 GND -> common ground


 * CP2102 USB Converter RX -> PD3 (TXD1)
 * Onboard LED -> PB0
 */


#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdio.h>
#include <util/delay.h>
#include <string.h>
#include <math.h>


#define PI 3.14159265
#define DEG_TO_RAD ((double)(PI/180.0))
#define RAD_TO_DEG ((double) (180.0/PI))


// _delay_loop_2(18432) = 0.01 s

double ax, ay, az;
double mx, my, mz;
volatile uint8_t iflag;
uint8_t istate;
uint8_t acount;
int32_t axreads;
int32_t ayreads;
int32_t azreads;

double xc, yc, y_ax_ay;

// Magnetometer calibration data
double m_xBias, m_sens_x;
double m_yBias, m_sens_y;
double m_zBias, m_sens_z;


// Accelerometer calibration data
double a_xBias, sens_x;
double a_yBias, sens_y;
double a_zBias, sens_z;


// Heel and pitch angles
double rho, phi;


// Buffer for accelerometer readings.
int16_t reads[3];


/*
 * This interrupt routine is called when the DRDY line
 * of the Micromag3 goes high
 */
ISR(INT5_vect)
{
   iflag = 1;
   EIMSK &= ~(_BV(INT5));  // disable interrupt on DRDY line
}


static int uart_putchar1(char c)
{
   loop_until_bit_is_set(UCSR1A, UDRE1);
   UDR1 = c;
   return 0;
}

void SPI_MasterInit(void)
{
   uint8_t i;

   for(i = 1; i < 100; i++)  // 1 s delay
      _delay_loop_2(18432);

   // Enable pull-up on PB4(CS0) and PE2(SSNOT)
   PORTB = _BV(PB4);
   PORTE = _BV(PE2);
  

   // Set PB0(LED), PB1(SCK), PB2(MOSI), PB5(RST0) and PB6(RST1) as output low
   // Set PB4(CS0) as output high
   DDRB = _BV(DDB0) | _BV(DDB1) | _BV(DDB2) | _BV(DDB4) | _BV(DDB5) | _BV(DDB6);
   // Set PE2(SSNOT) as output high
   DDRE = _BV(DDE2);

   // Enable SPI, Master, set clock rate fck/16
   SPCR = _BV(SPE) | _BV(MSTR) | _BV(SPR0); // 460.8 kHz

   for(i = 1; i < 100;i++)  // 1 s delay
      _delay_loop_2(18432);
 
   // release SCA3000 reset (PB5)
   PORTB |= _BV(PB5);

   for(i = 1; i < 25; i++)  // 0.25 s delay
      _delay_loop_2(18432);
}


/*
 * Start magnetometer measurement on axis 0(x), 1(y) or 2(z)
 */
void mag_start(uint8_t axis)
{
   PORTE &= ~(_BV(PE2)); // select Micromag3
   

   PORTB |= _BV(PB6); // pulse the reset (minimum 100 nanoseconds)
   asm volatile("nop\n\t"
                "nop\n\t"
                ::);
   PORTB &= ~(_BV(PB6));
  

   SPDR = 0x70 + axis + 1;
   while(!(SPSR & _BV(SPIF)));
  

   PORTE |= _BV(PE2); // deselect Micromag3
   istate = axis;  // save current active axis
   iflag = 0; // reset end-of-measurement flag
   EIMSK |= _BV(INT5);  // enable interrupt on DRDY line
}


int main(void)
{
   char buffer[64];
   uint8_t i;

   acount = 0;
   istate = 0;
   iflag = 0;

   for(i = 1; i < 100; i++)  // 1 s delay
      _delay_loop_2(18432);

   /* enable serial port UART */ 
   /* Set baud rate : 9600 bps @ 7.3728 MHz */
   UBRR1L = (unsigned char)(47);
   /* Enable transmitter */
   UCSR1B = _BV(TXEN1);

   SPI_MasterInit();

   // enable external interrupt on DRDY line
   EICRB = _BV(ISC50) | _BV(ISC51);

   sei();

   a_xBias = 278.0;
   a_yBias = 114.0;
   a_zBias = -131.5;

   sens_x = 1331.0;
   sens_y = 1334.0;
   sens_z = 1331.5;

   m_xBias = -49.5;
   m_yBias = 24.5;
   m_zBias = 26.0;

   m_sens_x = 3530.5;
   m_sens_y = 3498.5;
   m_sens_z = 3236.0;

   mag_start(0);  // start x-axis measurement

   while(1)
   {
      if(iflag)  // we have a magnetometer measurement
      {
         uint8_t mh, ml;
         int16_t mag;
  
         // read the result
         PORTE &= ~(_BV(PE2)); // select Micromag3
        

         SPDR = 0x00;
         while(!(SPSR & _BV(SPIF)));
         mh = SPDR;
         SPDR = 0x00;
         while(!(SPSR & _BV(SPIF)));
         ml = SPDR;
        

         PORTE |= _BV(PE2); // deselect Micromag3
         mag = (((int16_t)mh) << 8) + ml;
  
         switch(istate)  // which axis did we measure?
         {
         case 0:    // we just read magnetometer x-axis
            mx = (double)mag;
            mag_start(1);  // start y-axis measurement
            break;
         case 1:    // we just read magnetometer y-axis
            my = (double)mag;
            mag_start(2);  // start z-axis measurement
            break;
         case 2:    // we just read magnetometer z-axis
            mz = (double)mag;
   
            mx = (mx - m_xBias) / m_sens_x;
            my = (my - m_yBias) / m_sens_y;
            mz = (mz - m_zBias) / m_sens_z;
   
            // calculate and print the uncorrected heading
            double headbrut = 360.0 - atan2(my, mx) * RAD_TO_DEG;
            if(headbrut > 360.0)
               headbrut -= 360.0;
            sprintf(buffer, "%5.1f", headbrut);
            for(i = 0; i < strlen(buffer); i++)
               uart_putchar1((unsigned char)(buffer[i]));
   
            // calculate accelerometer average
            reads[0] = axreads / acount;
            reads[1] = ayreads / acount;
            reads[2] = azreads / acount;
   
            ax = (reads[0] - a_xBias) / sens_x;
            ay = (reads[1] - a_yBias) / sens_y;
            az = (reads[2] - a_zBias) / sens_z;
   
            // calculate heel(rho) and pitch(phi)
            rho = -atan(ay / sqrt(ax * ax + az * az)) * RAD_TO_DEG;
            phi = -atan(ax / sqrt(ay * ay + az * az)) * RAD_TO_DEG;
   
            // normalize accelerometer readings
            double norm = sqrt(ax * ax + ay * ay + az * az);
            ax /= norm;
            ay /= norm;
            az /= norm;
   
            // tilt compensation
            ay = -ay;
            double one_ax2 = 1.0 - ax * ax;
            y_ax_ay = my * ax * ay;
            xc = mx * one_ax2 - y_ax_ay - mz * ax * az;
            yc = my * az - mz * ay;
   
            // calculate and print corrected heading, heel, pitch
            double head_corr = 360.0 - atan2(yc, xc) * RAD_TO_DEG;
            if(head_corr > 360.0)
               head_corr -= 360.0;
            sprintf(buffer, "  %5.1f  %5.1f  %5.1f", head_corr, rho, phi);
            for(i = 0; i < strlen(buffer); i++)
               uart_putchar1((unsigned char)(buffer[i]));
            uart_putchar1('\r');
            uart_putchar1('\n');
   
            PORTB ^= 0x01;  // toggle LED
   
            axreads = 0;
            ayreads = 0;
            azreads = 0;
            acount = 0;
   
            mag_start(0);  // start x-axis measurement
         }
      }
 
      uint8_t azh, azl, ayh, ayl, axh, axl;  

      // read accelerometer
      // Select SCA3000
      PORTB &= ~(_BV(PB4));

      SPDR = 0x09 << 2;
      while(!(SPSR & _BV(SPIF)));
      

      SPDR = 0x00;
      while(!(SPSR & _BV(SPIF)));
      azh = SPDR;
      SPDR = 0x00;
      while(!(SPSR & _BV(SPIF)));
      azl = SPDR;
      SPDR = 0x00;
      while(!(SPSR & _BV(SPIF)));
      ayh = SPDR;
      SPDR = 0x00;
      while(!(SPSR & _BV(SPIF)));
      ayl = SPDR;
      SPDR = 0x00;
      while(!(SPSR & _BV(SPIF)));
      axh = SPDR;
      SPDR = 0x00;
      while(!(SPSR & _BV(SPIF)));
      axl = SPDR;
 
      // Deselect SCA3000
      PORTB |= _BV(PB4);
 
      reads[2] = ((((int16_t)azh) << 8) + azl) >> 3;
      reads[1] = ((((int16_t)ayh) << 8) + ayl) >> 3;
      reads[0] = ((((int16_t)axh) << 8) + axl) >> 3;
  
      axreads += reads[0];
      ayreads += reads[1];
      azreads += reads[2];
  
      acount++;
  
      _delay_loop_2(7089);    // 1/260 s delay
   }
}  

Tuesday, April 26, 2011

A custom gyro compass (Phase 2)

Here are some results of Phase 2 of this project, whose objective was to test a tilt-compensated compass in static conditions.

The compass actually consists of a single Micromag3 magnetometer and an SCA3000 accelerometer, arranged on a breadboard in the following configuration. The N+ means that the magnetometer reading is positive when the arrow points to magnetic North. Note the opposite directions of the Y-axis: this will be corrected in code by inverting the sign of the Y-axis accelerometer reading. The Z-axis points down in both cases.


The magnetometer has been calibrated using the same technique previously described for the accelerometer, with the following results:



The magnetometer reads continuously and consecutively the X, Y and Z axis. A complete measurement of the 3 axis takes about 105 ms. The microprocessor can take 31 complete accelerometer readings while waiting for the magnetometer to complete its measurements. The normalized average of these 31 measurements (ax, ay, az) is used in the calculations, along with the current magnetometer result (mx, my, mz).

The tilt-compensation equations are: 


In this first example, the breadboard is tilted on its side to produce a heel angle of around 35 degrees, and then slowly comes back to horizontal, as shown by the green curve. This produces a huge variation in the uncorrected heading (the blue curve). The tilt compensation does a decent job, but not good enough, as the red curve should ideally be horizontal. This means that the calibration should be improved.




The next example illustrates the dynamic response, as the breadboard is tilted from one side to the other. What is interesting here is that the tilt compensation is able to follow the moving compass without problem.



 The next step will be to implement more robust calibration procedures for the magnetometer and the accelerometer, adding required corrections for the lack of perpendicularity of the axes and linearity of the responses.

Sunday, April 24, 2011

Tilt compensation: what to expect

In order to help in the calibration of a tilt-compensated compass, I have pre-calculated what are the uncorrected compass readings, for different heading and heel angles.

At my northern location, the horizontal and vertical components of the magnetic field are respectively 17.686 and 51.676 microteslas, meaning that the inclination of the field in the ground is 71.107°.

The results of these calculations are summarized in the following two figures, for positive and negative heel angles (with a zero pitch angle). Heel angle is considered positive when the mast leans to starboard.






It is interesting to note that there is one heading where the non-compensated compass will give a correct result, independantly of the heel angle: 270° for positive heel, and 90° for negative heel.

To reproduce these results for other locations, here are the equations to use.




Wednesday, March 30, 2011

Accelerometer calibration code

This is the microcontroller code that I used to calibrate the SCA3000 accelerometer as described in the previous post. This will send the measurements through the serial port at a baud rate of 9600.

/* Calibration routine for the SCA3000 accelerometer
 * using the WaveShare STK128+ Standard development
 * board with voltage level jumper set to 3.3 V

 * SCA3000 MOSI -> PB2(MOSI)
 * SCA3000 MISO -> PB3(MISO)
 * SCA3000 SCK -> PB1(SCK)
 * SCA3000 CSB -> PB4
 * SCA3000 RST -> PB5
 * SCA3000 INT (not connected)
 * SCA3000 VIN -> 5 V from USB port
 * SCA3000 GND -> common ground

 * CP2102 USB Converter RX -> PD3 (TXD1)
 * Onboard LED -> PB0
 */


#include <avr/io.h>
#include <stdio.h>
#include <util/delay.h>
#include <string.h>


// _delay_loop_2(18432) = 0.01 s

// Accelerometer readings (x, y, z)
int16_t readings[3];


void SPI_MasterInit(void)
{
   uint8_t i;

   for(i = 0; i < 100; i++)  // 1 sec delay
      _delay_loop_2(18432);

   // Enable pull-up on PB4(CS0)
   PORTB = _BV(PB4);
  

   // Set PB0(LED), PB1(SCK), PB2(MOSI) and PB5(RST0) as output low
   // Set PB4(CS0) as  output high
   DDRB = _BV(DDB0) | _BV(DDB1) | _BV(DDB2) | _BV(DDB4) | _BV(DDB5);

   // Enable SPI, Master, set clock rate fck/16
   SPCR = _BV(SPE) | _BV(MSTR) | _BV(SPR0); // 460.8 kHz

   for(i = 0; i < 100; i++)  // 1 sec delay
      _delay_loop_2(18432);

   // Release SCA3000 reset (PB5)
   PORTB |= _BV(PB5);

   for(i = 0; i < 25; i++)  // 0.25 sec delay
      _delay_loop_2(18432);
}


// Read accelerometer x, y and z axis
void readAccelerometer(int16_t* reads)
{
   uint8_t azh, azl, ayh, ayl, axh, axl;

   // Select SCA3000
   PORTB &= ~(_BV(PB4));

  
   SPDR = 0x09 << 2;
   while(!(SPSR & _BV(SPIF)));

   SPDR = 0x00;
   while(!(SPSR & _BV(SPIF)));
   azh = SPDR;
   SPDR = 0x00;
   while(!(SPSR & _BV(SPIF)));
   azl = SPDR;
   SPDR = 0x00;
   while(!(SPSR & _BV(SPIF)));
   ayh = SPDR;
   SPDR = 0x00;
   while(!(SPSR & _BV(SPIF)));
   ayl = SPDR;
   SPDR = 0x00;
   while(!(SPSR & _BV(SPIF)));
   axh = SPDR;
   SPDR = 0x00;
   while(!(SPSR & _BV(SPIF)));
   axl = SPDR;


   // Deselect SCA3000
   PORTB |= _BV(PB4);


   reads[2] = ((((int16_t)azh) << 8) + azl) >> 3;
   reads[1] = ((((int16_t)ayh) << 8) + ayl) >> 3;
   reads[0] = ((((int16_t)axh) << 8) + axl) >> 3;
}


// Take 64 readings and calculate the average
void readAverage(void)
{
   int32_t x_accu = 0;
   int32_t y_accu = 0;
   int32_t z_accu = 0;
    
   for (uint8_t i = 0; i < 64; i++)
   {
       readAccelerometer(readings);

       x_accu += readings[0];
       y_accu += readings[1];
       z_accu += readings[2];

       _delay_loop_2(7089);    // 1/260 s delay
   }

   
readings[0] = x_accu / 64;
   readings[1] = y_accu / 64;
   readings[2] = z_accu / 64;
}


static int uart_putchar1(char c)
{
   loop_until_bit_is_set(UCSR1A, UDRE1);
   UDR1 = c;
   return 0;
}


int main(void)
{
   uint8_t i;

   char buffer[64];

   for(i = 0; i < 100; i++)  // 1 second delay
      _delay_loop_2(18432);

   /* enable serial port UART1 */ 
   /* Set baud rate : 9600 bps @ 7.3728 MHz */
   UBRR1L = (unsigned char)(47);
   /* Enable transmitter */
   UCSR1B = _BV(TXEN1);

   SPI_MasterInit();

   
   while(1)
   {
      PORTB ^= 0x01;  // toggle LED

      readAverage();

      sprintf(buffer, "%5i %5i %5i", readings[0], readings[1], readings[2]);

      for(i = 0; i < strlen(buffer); i++)
         uart_putchar1((unsigned char)(buffer[i]));

      uart_putchar1('\r');
      uart_putchar1('\n');
   }
}


UPDATE :
I am now using an improved procedure for accelerometer calibration, which adds a correction for axis misalignments (cross-talk).
See:

 

Monday, March 28, 2011

Heel and pitch angle from accelerometer

In the development of my custom tilt-compensated compass, I have chosen to use the SCA3000 3-axis accelerometer.

One important benefit of having an accelerometer is that it can also provide the boat’s heel and pitch angles. But first, the accelerometer has to be properly calibrated.

The Freescale AN3447 Application Note (“Implementing Auto-Zero Calibration Technique for Accelerometers”) describes 4 different techniques to calibrate an accelerometer. In order of decreasing accuracy, they are:

1. Manual 0g X, Y, Z Full Range Calibration
2. Simple 0g X, Y, Z calibration
3. Freefall Calibration
4. Simple 0g X, 0g Y, +1g Z calibration

The last one (no. 4), although not the most accurate, is the more convenient to use and provides good performance in many applications. A complete implementation can be found here: http://mbed.org/cookbook/IMU.

But for my project, I definitively want to go with the most accurate technique (no. 1) in order to meet the design goals.

Here is a description of the setup used for the calibration.


I am using a development board with the new ATmega128A chip.


The ATmega128A chip can be operated at any voltage between 2.7 and 5.5 V, replacing both the ATmega128 (5V) and ATmega128L (3.3V). I am using the board at 3.3 V, with the onboard crystal of 7.3728 MHz. (There is room on the board for installing a 16 MHz crystal for 5V operation). The development board is powered from the USB port of a laptop.

The Sparkfun SCA3000 breakout board is connected trough the SPI bus plus a reset line. The breakout board is also powered from the USB port, as it has its own 3.3V voltage regulator that cannot be bypassed.

A calibration routine has been programmed in the ATmega128A that continually reads the accelerometer outputs, and after each bunch of 64 readings, send the average of each axis on the serial port with Hyperterminal showing the results on a laptop.

The calibration technique requires that you record the maximum and minimum values independantly for the 3 axis. The reason that we take an average over 64 values instead of an instantaneous value is to cancel the noise that would otherwise lead to overestimate the sensitivity. The AN3447 Application Note describes the process as tedious, and yes it is. You have to be patient and explore all the minute positions around the min and max values of each axis, while taking care of recording output values only when the accelerometer is completely at rest.

This is what I get after calibrating:




The sensitivity is calculated as (max- min)/2 and the 0-g bias as (max – sensitivity). In the datasheet of the SCA3000, the nominal sensitivity is reported as 1333 counts /g. From now on, the g-value for each axis will be calculated as:

               Ax (or Ay or Az) = (measurement – bias) / sensitivity.

Theoretically, we should find that SQRT(Ax2 + Ay2 + Az2  ) = 1. In practice, there will be a small difference that we can eliminate by normalizing the results. But it is not required if all we want is to calculate the heel and pitch angles, as the results will be the same with or without normalization.
The Freescale AN3461 Application Note (“Tilt Sensing Using Linear Accelerometers”) explains how you can get the best numerical resolution from a 3-axis accelerometer by calculating the heel and pitch angles as:  

Once calibrated, this is what I observe with the breadboad sitting still on the table.  This is a measurement of the actual alignment offset of the accelerometer vs the breadboard (in fact vs the ground) :
heel = -0.52 deg, pitch = 0.57 deg.



UPDATE :
I am now using an improved procedure for accelerometer calibration, which adds a correction for axis misalignments (cross-talk).
See:


Wednesday, March 23, 2011

Measuring boat speed (Part 2)

In the previous post, I described in a general way how the boat speed is measured. I present here the part of the microcontroller code that is used for this task.

volatile uint16_t icp3_start_time;
volatile uint16_t icp3_period;
volatile uint8_t zero_flag;
double boat_speed;
uint16_t period3;
uint8_t sreg;

/*
 * This interrupt routine is called each time
 * a new pulse rising edge is detected
 */
ISR(TIMER3_CAPT_vect)
{
   uint16_t icr3;

   uint16_t test_period;

   /*
    * Make a copy of the timer value saved in the ICR3 register
    * when the rising edge of the pulse occured
    */
    icr3 = ICR3;

   if(zero_flag)  // this is the first pulse of a new series
   {

      /*
       * Save the start timestamp in a global variable
       * for further use
       */
       icp3_start_time = icr3;

      /*
       * Push the timeout window forward by moving the OCR3A timeout
       * value just behind the capture timestamp so that the timer
       * will have to go through nearly a whole cyle before reaching OCR3A
       */
       OCR3A = icr3 - 100;

      /*
       * Next time, go to the 'else' section to calculate the time
       * interval unless a timeout has occured meanwhile, setting back
       * the zero_flag to 1
       */
       zero_flag = 0;
    }
    else
    {
       /*
        * Calculate the number of timer ticks since the last pulse
        */
        test_period = icr3 - icp3_start_time; // length of previous period

       /*
        * This is a debouncing test that is probably not required, but
        * safe to keep. If icp_period is less than 200, this means that
        * the nominal boat speed will be calculated at more than 16.6 knots,
        * an indication of a false or noisy pulse that should be discarded
        */
        if(test_period > 200)   // if we have a good pulse
        {
           icp3_period = test_period;

           icp3_start_time = icr3;    // icr3 becomes the new start time
           OCR3A = icr3 - 100;     // move the timeout window
        }
    }
}
¸
/*
 * This interrupt routine is called when the timeout
 * period is reached, i.e. TCNT3 = OCR3A (boat speed less than 0.05 knot)
 */
ISR(TIMER3_COMPA_vect)
{
   // tell everyone that we have a zero speed
   zero_flag = 1;
}

void init_timer(void)
{
   // Set timer/counter 3 with a prescale of 1024
   // and with input capture on rising edge
   TCCR3B = _BV(ICES3) | _BV(CS32)| _BV(CS30);
   TCNT3 = 0;    // initialize timer/counter 3
   ETIMSK |= _BV(TICIE3);    // enable input capture interrupt

   OCR3A = 0;  // inialize timeout value
   ETIMSK |= _BV(OCIE3A); // enable output compare interrupt
}

int main(void)
{
   // ...

   zero_flag = 1;

   init_timer();

   /* enable interrupts */
   sei();

   boat_speed = 0.0;

   for(;;)
   {
     // begin a new cycle

     // ...

     // calculate the measured boat speed
     if(zero_flag)
        boat_speed = 0.0;
     else
     {
        sreg = SREG;

        cli();  // disable interrupts while we transfer a volatile 16-bit value
        period3 = icp3_period;
        SREG = sreg;  // re-enable interrupts

        boat_speed = 1.0 / (period3 * 0.000064 * 4.7);

        // get the calibration correction factor
        // ...

        // apply the calibration correction factor
        boat_speed *= calbat;
     }

     // put the result in the data structure
     dump_info.mbs1 = boat_speed;

     // ...

     // Wait for timer signal to begin a new cycle

     // ...
   }

Measuring boat speed (Part 1)

The boat speed is measured with Airmar ST650 speed sensors (similar to the newer ST850). The sensor produces two pulses per revolution of the paddlewheel. The following figure illustrates the interface between the speed sensor and the microcontroller. The ouput signal is fed directly to an input pin of the microcontroller, without further conditioning.  The pulses are detected as clean changes in logic level by the internal circuitry of the microcontroller. The thermistor is not used here.



The nominal pulse rate of the ST650 (without fins) is 4.7 Hz per knot.  Here is the form of the signal produced for a nominal speed of 3.0 knots.



Conversely, by measuring the time between the rising edges of two pulses, we can calculate the corresponding boat speed:
                                            
                              Speed (knots) = 1 / (delta_t * 4.7)

To measure the time interval, we set up a 16-bit timer running at 16 MHz / 1024 = 15625 Hz. This timer has a resolution of 1/15625 = 0.000064 second. The timer counts from 0 to 65535 then overflows to 0 and up again to 65535. Its time span is thus 65536 * 0.000064 = 4.19 seconds, which corresponds to a nominal boat speed of 1 / (4.19 * 4.7) = 0.05 knot. If the time span between 2 pulses is longer than that, we put the speed to zero. As an example, when the speed is 3.0 knots, there are 0.0709 / 0.000064 = 1108 timer ticks between each pulse. To get the boat speed, we count the timer ticks between each pulse and multiply them by 0.000064 s to get the time interval. We can then calculate the nominal boat speed, which will be multiplied later on by a calibration factor to get the final MBS (measured boat speed).

To implement this in the ATMega128, we use the input capture facility that comes with the internal 16-bit timers. When the rising edge of a new pulse is detected, the current timer value is saved in a special register, and an interrupt is fired. During the interrupt service routine, we calculate the timer counts since the last pulse and put the result in a variable that will be used by the main program to calculate the final boat speed of the current cycle. In the same interrupt routine, we also calculate what will be the timer value 4.19 seconds later. This is a timeout value that we push forward at each new pulse, and should not be attained as long as the boat speed is greater than 0.05 knot.

We set up a second interrupt that fires if the timeout value is attained. In the corresponding interrupt service routine, we set a flag to indicate to the main program that the speed is zero, and that the next pulse will be the first of new series.

All these commentaries will be helpful to follow the code snippets that I will present in Part 2 of this post.