How to make a
GPS-based autopilot
control head
for your boat

    Galleon     Contact me.

This page updated: August 2014



Overview
Parts
Test Programming
Programming


Current project status:
The program is "done", but goes crazy when board is connected to actual auto-pilot back-end and tested live. Noise is coming from back-end relays and into PIC board through the power connection.

Sorry, I've given up on this project; I never got it fully working on my boat. Some kind of electrical glitch made the board reset every time it steered. The program works, but some people say you may have to change it to read a different sentence from the GPS to steer properly. And now my GPS's display has faded out, so I won't be trying to get it to work any further. Sorry.

From someone 6/2014:
After many modifications, debugging and some basic steering algorithm, I got the system working !! Code is Oper_Full_PIC16F648a_Rev4.

But, working with GPS has its price. I am now in a point that the stability and accuracy of steering is limited by update rate of the GPS data that I found to be too slow for sea condition higher than calm.

I am now thinking about replacing the GPA with a "tilt compensated compass", they are cheap enough for me to give it a try.

From someone else :
I also tried steering by GPS ... and it wasn't good. Data rate is much too slow ... by the time GPS notices I'm off course ... I'm WAY off course so it steers an extreme zig-zag route along my intended path.

My understanding of commercial autopilots is that when steering by GPS, they simply use the GPS to create a local compass bearing (updated periodically) and steer by digital compass and pitch/yaw from accelerometers.




Overview



My 35-year-old sailboat has a 35-year-old below-decks autopilot (Benmar 16B-3). The "control" parts of the autopilot are complicated, a mix of transistor-era elecronics and odd mechanical connections, and very balky and mostly broken. The "movement" parts of the autopilot (an electric motor, hydraulic pump, and hydraulic ram) still work okay.

Alternatives:
  1. Get the autopilot repaired by the manufacturer.

    Likely to be expensive, if feasible at all. And afterward I'll have a repaired 35-year-old autopilot.

  2. Buy a complete new modern below-decks hydraulic autopilot.

    Expensive; probably $2000 ?

  3. It might be possible to buy just the control head part (compass and buttons and intelligence electronics) of a modern autopilot and connect that to the "movement" parts of my old autopilot. The control head itself would cost $100-$200 used, maybe $400 new, and then a small dab of additional electrical stuff might be needed to connect it to my existing electrical motor.

  4. Make a full control head:

    Design and make a small circuit board containing a magnetic or fluxgate compass sensor or a GPS module, and control relays that can control the electric motor of my autopilot. I could design it simply to keep steering in whatever direction the boat was headed when the board was turned on, so it wouldn't need any kind of "user interface" (buttons and LEDs and display to let the user control it). Total parts cost might be $150-$300.

    But my circuit-design and circuit-building skills are nil. This also requires computer-programming skills; I have those.

  5. Connect GPS to "movement" parts of my old autopilot.

    I already have a GPS that can output NMEA 0183 information to steer to a waypoint. That is most of the "intelligence" of an autopilot control head; maybe I can connect that to the "movement" parts of the old autopilot.

    What is needed is some electronics that can read NMEA 0183 signals from the GPS and produce "steer right" and "steer left" electrical outputs to the electrical motor from my old autopilot.

I chose option number 5.



GPS-to-steering Alternatives:
  1. Commercial products:

    I have found one cheap-ish commercial product that connects GPS to steering: the GPS Smart Coupler.

    But it costs $250, and the output signals from it won't control an electrical motor directly (without some additional relays or other electrical stuff). And things could get complicated if the "steering sensitivity" of the two parts don't match.

  2. Make it myself:

    I could buy and program a small circuit board that will read NMEA 0183 output from my boat's GPS and control relays that can control the electric motor of my autopilot. Total parts cost might be $100.

    This requires computer-programming skills; I have those.

I chose option number 2.



Parts



  1. GPS that can steer to a waypoint and output NMEA sentences.

    Looks like the GPS must output the "GPRMB" sentence, which contains the "cross-track error" info.
    Example: $GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*0B<CR><LF>
    where "0.66,L" (always exactly 6 digits) is the cross-track error in NM and direction to steer.

    My boat has a Garmin 128 GPS, which can do this.
    (Some GPS's may provide a "GPAPB" or "GPXTE" sentence instead, which gives same "cross-track error" info. Some GPS's may provide proprietary sentences, starting with "P", to do the same.)

    Not sure how often these sentences are output by each GPS.

    Not sure what the Data Status (A/V) field means; one document says "A = OK, V = warning", another says "A = OK, V = Void (warning)", another says "A = Active, V = Void". Does that mean "A = on course, V = off course", "A = good info, V = ignore this sentence", or something else ?

    GPS NMEA 0183 output (RS-232) must be +/-12 volts; I've heard some (non-GPS) devices cheat and put out +/-5 volts.
    [VERIFY THAT MY GPS'S NMEA OUTPUT LEVEL IS +/-12 VOLTS; MANUAL DOESN'T SAY.]


  2. Circuit board (development board) that can read NMEA 0183 (RS-232) input and control relays for motor-control.

    I chose to use PIC-based boards, and bought this one:
    Microchip PIC-IO-A or PIC-IO-18 ($33; 18-pin; 4 x 10A relays)

    (I think my autopilot motor draws about 5A at 12VDC, well within the relay capacity. And I can gang together two relays for each steering direction, so I'd be using only 5A of a 20A capacity. My old autopilot does have a relay box for the motor, but it's fairly complicated and I'm not sure I want to rely on it.)

    12V input
    Status LED connected to RB5 (PORTB bit 5; marked "LED9" on board)
    Input status LEDs (marked "LED1" through "LED4" on board; RA4 = LED1/IN1, RB0 = LED2/IN2, RB3 = LED3/IN3, RB4 = LED4/IN4)
    Output/relay status LEDs (marked "LED5" through "LED8" on board; RA3 = LED5/RELAY1, RA2 = LED6/RELAY2, RA1 = LED7/RELAY3, RA0 = LED8/RELAY4)
    (Had to get the schematic image for the board to figure out the pin-to-LED assignments.)


  3. Microcontroller chip to plug into the circuit board:

    I chose PIC-type processors:
    (Must have one with a UART or USART, for doing RS-232.)
    (Not sure what memory size I'm going to need.)

    Wanted SparkFun PIC 16F88 (4K; $5), but ended up buying everything from Microchip, so got:
    PIC16F628A-I/P from Microchip (2K; $3; 18 DIP; 20 MHz)


  4. A PC (desktop or laptop) for writing and downloading the program that will run on the processor chip:

    I have a laptop with a USB port, no RS-232 port.
    This turns out to be important when you get to the "choose a programmer" step; programmers using USB are more expensive than those using RS-232, but laptop RS-232 ports or USB-to-RS-232 adapters may not work.

  5. Software to create the program that will run on the processor chip:

    I chose to write in the C language, so I need a C compiler that supports the chip I chose:
    HI-TECH C PRO for the PIC10/12/16 MCU Family (free)
    MPLAB Integrated Development Environment (free; includes debugger, simulator, two C compilers, interfaces to programmer boards)
    [DO I WANT TO TRY A SIMULATOR ?]

  6. Programmer board to install program from PC into PIC processor chip:

    (Programmers vary by: chips supported, type of connection to PC, type of power source, type of connection to chip or development board.)

    SparkFun PGM-00004 ($92; USB)
    Futurlec ($59; USB)

    I bought the Microchip PX-200.

    Had to buy special cable to connect programmer to development board; apparently there are no standards, so cable that came with programmer wasn't right. Cable cost $9.50 plus $4 shipping. Use beige RS232 cable, not grey one, and pins are connected as follows: MCLR on small board to pin 1 on PIC-IO-18 board, rest in order up to PGM on small board to pin 6 on PIC-IO-18 board.

  7. Software to drive programmer board:

    PICkit 2 came included with the programmer board, and is used by MPLAB IDE.

    IC-Prog (free)
    [DON'T SEE ANY OF THE USB PROGRAMMERS ABOVE ON THE IC-PROG SUPPORTED LIST]


  8. Miscellaneous:

    Male DB-9 connector to plug into circuit board, for NEMA input from GPS.
    Various wire.
    12V power plug to fit socket on target board.
    Power switch.
    Box to mount/hold/shield the circuit board.



Various resources for knowledge and parts:

Picture of programmer and target boards.



Test Programming



12/2011: Someone has told me that in newest software,

include file name has been changed from pic16f62xa.h to pic16f628a.h,
and also

__CONFIG(UNPROTECT & LVPDIS & BORDIS & MCLREN & PWRTDIS & WDTDIS & HS); // 0x3F2A
changes to
__CONFIG(CP_OFF & LVP_OFF & BOREN_OFF & MCLRE_ON & PWRTE_OFF & WDTE_OFF & FOSC_HS); // 0x3F2A

I haven't tested these changes myself.




Small test program (in C programming language):
// Test program to blink LEDs on PIC-IO-18 (PIC-IO-A) board
// with PIC16F628A chip installed.

#include <pic.h>
#include <pic16f62xa.h>
#include <htc.h>	// required for delay routines

__CONFIG(UNPROTECT & LVPDIS & BORDIS & MCLREN & PWRTDIS & WDTDIS & HS);  // 0x3F2A
/* for PIC-IO-18 board, oscillator configuration == HS */

#ifndef _XTAL_FREQ
// required to calibrate __delay_us() and __delay_ms()
#define _XTAL_FREQ 4000000	// internal clock freq (4MHz), not freq (20MHz) of crystal on board
#endif

/*
for PIC-IO-18 board:
	output: RB5 = status LED9
	inputs: RA4 = LED1/IN1, RB0 = LED2/IN2, RB3 = LED3/IN3, RB4 = LED4/IN4
	outputs/relays: RA3 = LED5/RELAY1, RA2 = LED6/RELAY2, RA1 = LED7/RELAY3, RA0 = LED8/RELAY4
*/

main(void)
{
	unsigned int	i;

	TRISA = 0;		// all port A bits output
	TRISB = 0;		// all port B bits output
	PORTA = 0x00;		// turn all outputs off
	PORTB = 0x00;		// turn all outputs off

	for(;;) {
//		RA3 = 1;			// LED5/RELAY1
		RB5 = 1;			// LED9
		for(i = 50 ; --i ;) {
			__delay_ms(20);
		}
		RB5 = 0;			// LED9
		for(i = 500 ; --i ;) {
			__delay_ms(20);
		}
	}
}


Another small test program (in C programming language):
// Program to test timer interrupts
// on PIC-IO-18 (PIC-IO-A) board
// with PIC16F628A chip installed.

#include <pic.h>
#include <pic16f62xa.h>

__CONFIG(UNPROTECT & LVPDIS & BORDIS & MCLREN & PWRTDIS & WDTDIS & HS);  // 0x3F2A
/* for PIC-IO-18 board, oscillator configuration == HS */

/*
for PIC-IO-18 board:
	output: RB5 = status LED9
	inputs: RA4 = LED1/IN1, RB0 = LED2/IN2, RB3 = LED3/IN3, RB4 = LED4/IN4
	outputs/relays: RA3 = LED5/RELAY1, RA2 = LED6/RELAY2, RA1 = LED7/RELAY3, RA0 = LED8/RELAY4
*/

static long	lCount0;
static long	lCount1;
static bit	bLED0On;
static bit	bLED1On;

main(void)
{
	TRISA = 0;		// all port A bits output
	TRISB = 0;		// all port B bits output
	PORTA = 0x00;	// turn all outputs off
	PORTB = 0x00;	// turn all outputs off

	lCount0 = 0;
	lCount1 = 0;
	bLED0On = 0;
	bLED1On = 0;

	// Timer 0 is an 8-bit counter that interrupts
	// when it overflows from 0xFF to 0x00.
	// There is an 8-bit "prescaler", which counts
	// how many cycles (0-256) to ignore before incrementing
	// the counter each time.
	// This prescaler is shared with the watchdog timer.
	OPTION = 0b0111;	// Timer 0 prescale by 1:256
	T0CS = 0;		// Timer 0 increments on instruction clock
	T0IE = 1;		// Enable interrupt on TMR0 overflow

	// Timer 1 is a 16-bit counter that interrupts
	// when it overflows from 0xFFFF to 0x0000.
	// There is a 2-bit "prescaler", which counts
	// how many cycles (0-8) to ignore before incrementing
	// the counter each time.
	T1CKPS1 = 0;	// Timer 1 prescale by 1:1
	T1CKPS0 = 0;
	TMR1CS = 0;		// Timer 1 increments on instruction clock
	TMR1IE = 1;		// Enable interrupt on Timer 1 overflow
	TMR1ON = 1;		// Enable Timer 1

	// Timer 2: does not generate interrupts; feeds output
	// to serial port or CCP.

	PEIE = 1;		// enable peripheral interrupts; needed for Timer1
	GIE = 1;		// Global interrupt enable

	for(;;)
		NOP();		// do nothing
}


// interrupt function - the name is unimportant
static void interrupt
isr(void)
{
	// timer 0 overflow ?
	if (T0IF) {
		lCount0++;
		if ((lCount0%32) == 0) {
			bLED0On = !bLED0On;
			RB5 = bLED0On;		// LED9
		}
		T0IF = 0;		// clear interrupt flag, ready for next
	}

	// timer 1 overflow ?
	else if (TMR1IF) {
		lCount1++;
		if ((lCount1%128) == 0) {
			bLED1On = !bLED1On;
			RA3 = bLED1On;		// LED5/RELAY1
		}
		TMR1IF = 0;		// clear interrupt flag, ready for next
	}

	// unexpected interrupt
	else {
		RA0 = 1;			// LED8/RELAY4
	}
}


Another small test program (in C programming language):
// Program to test serial port receive interrupts
// on PIC-IO-18 (PIC-IO-A) board
// with PIC16F628A chip installed.

#include <pic.h>
#include <pic16f62xa.h>

__CONFIG(UNPROTECT & LVPDIS & BORDIS & MCLREN & PWRTDIS & WDTDIS & HS);  // 0x3F2A
/* for PIC-IO-18 board, oscillator configuration == HS */

/*
for PIC-IO-18 board:
	output: RB5 = status LED9
	inputs: RA4 = LED1/IN1, RB0 = LED2/IN2, RB3 = LED3/IN3, RB4 = LED4/IN4
	outputs/relays: RA3 = LED5/RELAY1, RA2 = LED6/RELAY2, RA1 = LED7/RELAY3, RA0 = LED8/RELAY4
*/

// do RS232 9600 baud, 8 bits
#define BAUD 9600
#define FOSC 20000000L
#define DIVIDER ((int)(FOSC/(16UL * BAUD) -1))// for >= 9600 baud
//#define DIVIDER ((int)(FOSC/(64UL * BAUD) -1))// for < 9600 baud

static unsigned char cInput;

static long	lCount;
static bit	bLEDOn;

main(void)
{
	unsigned char cInput;

	TRISA = 0;		// all port A bits output
	TRISB = 0;		// all port B bits output
	PORTA = 0x00;	// turn all outputs off
	PORTB = 0x00;	// turn all outputs off

	lCount = 0;
	bLEDOn = 0;

	// initialize communications
	TRISB = 0x06;	// RB1 and RB2 used by USART
	SPBRG = DIVIDER;// set baud rate
	RCSTA = 0x80;	// serial port enable, 8-bit
	TXSTA = 0x04;	// 8-bit, no-xmit, async, high-speed
	RCIE = 1;		// enable recv interrupts
	TXIE = 0;		// no xmit interrupts
	CREN = 1;		// continuous recv enable

	PEIE = 1;		// enable peripheral ints; needed for USART
	GIE = 1;		// Global interrupt enable

	for(;;)
		NOP();		// do nothing
}


// interrupt function - the name is unimportant
static void interrupt
isr(void)
{
	// received char ?
	if (RCIF) {
		if (FERR) {
			// framing error
			unsigned char cGarbage = RCREG;	// clears FERR
			RA1 = 1;		// LED7/RELAY3
			RCIF = 0;
			return;
		}
		lCount++;
		cInput = RCREG;
		if (cInput == 'G') {
			RA3 = 1;		// LED5/RELAY1
			NOP();
			NOP();
			RA3 = 0;		// LED5/RELAY1
		}
		bLEDOn = !bLEDOn;
		RB5 = bLEDOn;		// LED9
		if (OERR) {
			// overrun error
			CREN = 0;
			CREN = 1;
			RA2 = 1;		// LED6/RELAY2
		}
		RCIF = 0;		// clear interrupt flag, ready for next
	}

	// unexpected interrupt
	else {
		RA0 = 1;			// LED8/RELAY4
	}
}


Wiring for inputs to PIC-IO-18 (PIC-IO-A) board:
From Volker at Tech Support at MicroController Pros Corporation:
> Do the IN's take 5V or 12V ? I assume 5V.
> For example, +5 to IN1-1 and VSS to IN1-2 ?
> What is the maximum voltage tolerated ?
> If I had an external 12V supply, and needed to use
> a resistor in series to drop it down to 5V to apply
> to the Input, what size resistor should I use (what
> is the current draw through an Input) ?
> Where can I easily tap into GND and +5 on the
> board (without soldering or something),
> to route them to the IN's ?
> There doesn't seem to be any easy access point.

The PIC-IO-A board uses 4N37 optocouplers on the inputs (see schematics).

An optocoupler has an internal LED on the inputs, whose light output is proportional to the input current. You can apply ANY voltage you like, as long as you limit the diode forward CURRENT to the maximum allowable diode current as per 4N37 datasheet. You limit the diode current through a series resistor. Per 4N37 datasheet the MAX diode forward current is 60 mA.

Looking at the PIC-IO schematic you can see that there is an input indicator LED in series with the optocoupler LED. Both of these LEDs have a forward voltage of about 1.5V, so 2 x 1.5V = 3V.

In series with those two LEDs is a 330 Ohm current-limiting resistor. Therefore the voltage that drops over that current limiting resistor is Vr = Vin - 3V.

And the current that flows through the resistor and the diodes is I = Vr/R = (Vin - 3V) / R.

If Vin = 12V then I = 9V / 330 Ohm = 27 mA.

This is well below the max 60 mA forward current of the optocoupler, but it is on the high side for the input indicator LED. Normally you do not want to exceed 20 mA forward current for indicator LEDs.

So the solution would be to replace the 330 Ohm series resistor with a slightly higher value if you want to use a 12V input signal: 470 Ohms. 9V / 470 Ohms = 19 mA.

And yes IN1-1 is the positive input terminal; IN1-2 is GND.

Note that the optocoupler inverts your input signal. If you apply a positive voltage, the optocoupler output transistor will switch on, pulling the output to GND. The absence of a positive input voltage will turn the output transistor off, switching the output to high.

What is the quickest and dirtiest way I could use the inputs, on my boat ? My system voltage varies from about 12.3V (low batteries, no charging) to 14.5V (full batteries, alternator charging). Suppose I wired two of the board's inputs in series ? Each of the four LEDs (two on the board and two inside the optocouplers) drops 1.5V. So that would leave 14.5 - 6 = 8.5V to drop over the two 330 Ohm resistors in series. Current would be I = 8.5V / 660 Ohm = 12.9 mA. That would be well within the 20 mA limit for each indicator LED. Should work. Does work !

What is the maximum allowable input voltage on a single input ? Voltage drop through two LEDs will be 3V. Max current is 20 mA. Voltage drop through resistor will be V = 20 mA x 330 Ohm = 6.6 V. Max voltage to input V = 6.6V + 3V = 9.6 V.

Another small test program (in C programming language):
//--------------------------------------------------------------------------
// Test program to read inputs and light LEDs on PIC-IO-18 (PIC-IO-A) board
// with PIC16F628A chip installed.

// At startup, should see LED9 blink and all relays turn on.  Then when
// input voltage is applied to IN1 through IN4, should see corresponding
// input indicator LED light, and corresponding relay de-activate.

// Important: maximum voltage to a single input is 9.6V; otherwise
// you will exceed allowed 20 mA current through LED.

// To use this program with the standard board and no additional
// components (no additional drop-down resistors) in a nominally "12-volt"
// system, you can wire each set of two inputs in series, so that the
// voltage across each input is nominally 6 volts.  Positive goes to
// top of input 1, negative to bottom of input 2, bottom of 1 connected
// to top of 2.  Similarly, positive to top of input 3, negative to bottom
// of input 4, bottom of 3 connected to top of 4.

//--------------------------------------------------------------------------

#include <pic.h>
#include <pic16f62xa.h>
#include <htc.h> // required for delay routines

__CONFIG(UNPROTECT & LVPDIS & BORDIS & MCLREN & PWRTDIS & WDTDIS & HS); // 0x3F2A
/* for PIC-IO-18 board, oscillator configuration == HS */

#ifndef _XTAL_FREQ
// required to calibrate __delay_us() and __delay_ms()
#define _XTAL_FREQ 4000000 // internal clock freq (4MHz), not freq (20MHz) of crystal on board
#endif

/*
for PIC-IO-18 board:
output: RB5 = status LED9
inputs: RA4 = LED1/IN1, RB0 = LED2/IN2, RB3 = LED3/IN3, RB4 = LED4/IN4
outputs/relays: RA3 = LED5/RELAY1, RA2 = LED6/RELAY2, RA1 = LED7/RELAY3, RA0 = LED8/RELAY4
*/

bit bLED9 = 0;

//--------------------------------------------------------------------------

main(void)
{
unsigned int i;

TRISA = 0x10;	// RA4 is input; all other port A bits output
TRISB = 0x19;	// RB0/RB3/RB4 are inputs; all other port B bits output
PORTA = 0x00;	// turn all outputs off
PORTB = 0x00;	// turn all outputs off

for(;;) {

	RB5 = bLED9;
	bLED9 = (!bLED9);

	for(i = 10 ; --i ;) {
		__delay_ms(20);
	}

	// Want to do this:
	//RA3 = RA4;	// RELAY1 = IN1
	//RA2 = RB0;	// RELAY2 = IN2
	//RA1 = RB3;	// RELAY3 = IN3
	//RA0 = RB4;	// RELAY4 = IN4

	// But can't read and write individual bits so quickly; have to gang it up
	unsigned char cPortA;
	unsigned char cPortB;
	unsigned char cNewPortA;
	cPortA = PORTA;
	cPortB = PORTB;
	cNewPortA = 0;
	//RA3 = RA4;	RELAY1 = IN1
	if (cPortA & 0x10)
		cNewPortA |= 0x08;
	//RA2 = RB0;	RELAY2 = IN2
	if (cPortB & 0x01)
		cNewPortA |= 0x04;
	//RA1 = RB3;	RELAY3 = IN3
	if (cPortB & 0x08)
		cNewPortA |= 0x02;
	//RA0 = RB4;	RELAY4 = IN4
	if (cPortB & 0x10)
		cNewPortA |= 0x01;
	PORTA = cNewPortA;
}
}

//--------------------------------------------------------------------------


Problem when using PIC-IO-18 (PIC-IO-A) board relays to drive a load:
From me to Tech Support at MicroController Pros Corporation:
I have two of the relays connected to a motor-relay box, which itself contains relays that control an electric motor. This is on a boat, for an auto-pilot. The electric motor draws about 5A at 12VDC; a digital battery monitor on the 12VDC system doesn't show any big voltage drop when the motor runs. The battery system contains four golf-cart batteries (plenty of capacity to run a 5A motor intermittently). The motor-relay box comes from an old auto-pilot, is pretty simple, and works fine (as far as I can tell).

My program on the PIC-IO-A runs fine, until it closes either relay on the PIC-IO-A, which closes a relay in the motor-relay box, and makes the motor spin. Then the program goes crazy, probably hanging or something.

This happens even with an extremely simple program, one which just loops, calling delay() functions and turning LED9 and one of the relays on and off. No use of interrupts, serial port, or the four inputs.

What could cause this ? I thought the PIC-IO-A relay outputs are completely isolated from anything else on the board; nothing done there could affect the running of the program. I'm not trying to suck power from the PIC-IO-A board to apply to the motor-relay box. The only connection between the two is that both board and box get power from the same 12VDC system, which includes batteries, solar panels, refrigerator, etc.

Does turning on one of the relays feed back into the processor in any way ? Interfere with a timer, or cause an interrupt ? I'm using relays 1 and 2; haven't checked to see if the same happens with 3 and 4. In any case, I'd expect the same failure with the motor-relay box turned off, and that doesn't happen. Something to do with current drawn through the relay outputs, or voltages seen there ? Shouldn't be possible.

Do I need some kind of power-conditioning on the 12VDC coming into the power connector on the PIC-IO-A board ? But the board runs fine with lots of other stuff active in the 12VDC system: alternator charging batteries, refrigerator running, etc. Somehow, using the relay outputs to make the motor run is the key to causing the failure. (Again, the motor is not connected directly to the PIC-IO-A relays; there is another set of relays between the two. Don't know how much current it takes to activate those relays.)
Response from Tech Support:
Whenever you switch a relay some pretty nasty electromagnetic interference is generated. As EMC is radiated (it is not tied to wire connections) it can make its way into the any of the PIC pins, Vcc line or I/Os and cause corruption of internal registers.

There are ways around this. One would be to use the PIC's integrated brown-out reset and watchdog. For the watchdog you will have to write a small piece of software that reloads the watchdog timer before it times out. If it times out, it forces a reset. This way if your code ever gets stuck in an endless loop (due to corrupted registers), the chip can self-recover. The brown-out will detect any voltage drops that go below the set threshold level and also generate a reset.

Another way is shielding the PIC-IO board (don't think that would be necessary though).

Switching your relays may also cause voltage drop spikes that are so short you can't see it with a normal voltmeter, but that can very well affect the PIC's operation.

Try adding additional filtering to the 12V supply line that goes to the PIC board. I suggest a series inductor, then a 100uF capacitor to GND in parallel with a small ceramic cap (100nF). See if that improves things.
From me to Tech Support:
I did a further experiment: I disconnected the electric motor I'm trying to control, the one that draws 5A at 12VDC. So now have just relays on the PIC board making relays in my motor-box click. Still have the problem. Should be very little current flowing.

Board doesn't seem to be resetting; my program would indicate that via LED9. Seems to be hanging or looping or something.

I'm surprised the PIC board would be so vulnerable to fairly small currents/voltages on the outputs of the relays. Seems like a design flaw.
Response from Tech Support:
Again it may not be the currents flowing, but the electromagnetic interference or surges created by the relays. Enable the watchdog (and service it of course). The hanging is most likely a corrupted program counter and the watchdog will force a reset in that case and allow your code to recover. Without watchdog, no reset, you're stuck in the endless loop forever.

A non-decoupled relay can induce/cause big surges (rise and drops) in the supply voltage WITHOUT any current flowing. How do you know that you don't have those surges on your supply line? Have you hooked up a storage scope with at least 200MHz bandwidth and looked at the Vcc line when your relays are switching? Even a 1ns surge (that a low-bandwith scope won't be able to resolve) can cause digital electronics to hang up.

If it is caused by EMI, than this is NOT something you can "see" - the same way that you can't see radio waves - unless you have very specialized equipment to do EMI measurements (a shielded room and a spectrum analyzer with EMI measuring probe).

Each time a relay switches, a spark (like a small lightning bolt) is created between contacts and that not once, but hundreds of them as the contacts bounce. Worn out relays make this effect even worse. These electric arcs create strong electromagnetic signals over a frequency range up to the hundreds of MHz. If those make it inside the micro, bits flip and registers get corrupted, that's why they came up with the concept of having a watchdog integrated. Switching relays is about the most hostile environment to which you can subject digital electronics. We used a rig made of relays to test our new chip designs for EMI/EMC performance at the company I worked previously. It never failed to make the chips hang up eventually.

How old are your relays? Are they decoupled the way the PIC-IO relays are via surge diodes (see the PIC-IO schematic to see what I'm talking about) ? How close (distance) to your relays is the PIC-I/O board ? How are the Grounds of your different items connected ? Are you daisy-chaining the ground from one to the next or does each have its own ground line going back to the same, single point of connection ?

Have you implemented the suggestions I made in my first response ?
- Use watchdog ?
- Decouple power supply into the PIC-IO board ?

The PIC-IO board works fine, if what is attached to it is properly designed and decoupled.

Another easy experiment you can do is to temporarily power the PIC-IO board by its OWN independent power supply. If the code still locks up, then your problem is most likely with EMI/EMC.

If using an independent power supply solves your problem, then the interference comes via the power supply line (or Ground line) and decoupling it like I suggested should fix that; eventually you will need another inductor in the GND line. You should also make sure that the Gnd of all your items are connected together at a single point of connection, don't run gnd in a string/daisy-chain fashion.

All of above of course assumes that there is no bug in your code that could also cause the program to hang-up.

Suggest you read up on EMC/EMI in relation to relays and what to do to prevent it.
From me to Tech Support:
Thanks for all of the info. I need to read up and learn, and try some things. Turns out my motor-box relays do have quenching diodes across the inputs; don't know if they're working. Separate power-supply will be difficult on my boat.
Response from Tech Support:
Separate power supply not permanently, only for test to narrow down possible problem root cause. Use a separate 9V block battery, or something like that to power the PIC-IO board. The block battery will not last more than an hour but for a test that should be sufficient.

From Doug Hall:
If the relays don't have a "catch" diode on the relay coil, they will generate a significant spike when they are turned off. The property of an inductor is that it wants to maintain the current going through it (V = L di/dt) so when the voltage is removed a fairly high pulse develops across the coil for an instant. This pulse can induce noise into adjacent lines. In some applications a diode will be placed "backwards" across the relay coil to quench this spike. The spike can reach hundreds of volts, and the resulting noise can cause problems similar to what you're seeing.

More info here: http://www.kpsec.freeuk.com/components/relay.htm and here: http://www.bcae1.com/relays.htm

From Robert Sparks:
I am not an electronics expert but have a suggestion. If possible, have a look at the relay coils and consider placing a moderate value capacitor across the coil activating inputs. Reason is, when a a coil is energized, a magnetic field is generated around it. The initial magnetic pulse can induce voltage in a host of unrelated/unconnected circuits in physical or electrical proximity to the relay. That induced voltage tendency is greatly magnified when the relay is opened and the magnetic field collapses around the coil. A capacitor across the coil, transparent to DC voltage, would absorb some of that spike, reduce the speed of field collapse, and thus reduce the magnitude of the field. It's a cheap and easy try and it might work.

PS: One way to check whether induced voltage is the problem is to unsolder one side of the relay input coil, thus removing it from the DC circuit. If you have more than one relay, do one at a time leaving each one in turn out of the circuit until the problem (hopefully) goes away.

From ed:
Consider CEMF spikes on the feed lines to the relays. These can be corrected by: clamp diode or recirculating diode.

This condition will induce up to 20 or more times the original voltage back to the voltage source. When the relay is open and closed it causes the collapsing of the relay coil voltage thus producing a oppose voltage flow. Solution: place a small capacitor across the relay leads with proper lead connection to direct the reverse voltage to system ground.

Counter Electro Motive Force (CEMF)

From me:
Powering the PIC board from a 9V battery fixed the problem. So the noise is coming from my back-end auto-pilot relays, not the relays on the PIC board, and is coming into the PIC board through the power connection.

...

I tried getting grab-bags of capacitors and inductors and adding them to the back-end auto-pilot relays, to try to stop the noise. Didn't work. So I ended up buying a rechargeable 12 VDC 4.5 AH battery to run the circuit board; that seems to work. If a 9 V battery (625 mAH, total of 5.6 W) runs the board for 1 hour, maybe this battery (12 V and 4.5 AH, total of 54 W) will run the board for 8 to 10 hours ?

From Batur Can:
Hi Bill. i am trying diy autopilot like you. I found your project on the net. It is fit in my mind. I think you have a noise problem on the power line. I think you tried many options but i have a suggestion. When i have this kind of problems i use hi capacity capacitors. My idea put capacity between 14 and 5 pins to the pic. Capacity 4700uf *2 or 4 16volt. Connect 2 or 4 4700uf parallel and parallel 0,1 uf. Solder this group to the pins directly ( 5 is ground 14 is positive ) maybe back side of the board. Some times this solution works for suppress the spikes. Remove the relays from the board. If you have a relay group for drive the motor use an buffer uln2803 chip for drive the relays.




Power input to PIC-IO-18 (PIC-IO-A) board:
From Volker at Tech Support at MicroController Pros Corporation:
> What is the maximum input voltage for the nominally 12 VDC
> power connector ? I have a wind-generator that sometimes
> drives system voltage to 16 VDC.

If you look at the board schematic, you will see that portions of the board are powered DIRECTLY (without voltage regulator in between) from the 12 VDC - namely the coils of the relays, the transistors driving them and the LEDs indicating that the relays are switched on.

The critical part is the max specs of the relay, which you can see here: spec.

As you can see the relay coil voltage is rated at 12 VDC nominal and 130% VDC MAX. 130% of 12 V is 15.6 VDC MAX. If you exceed this voltage you risk damaging the relays. That damage may not occur immediately, but will rather manifest itself in a failure over time as the stress accumulates.

The LEDs will also be stressed beyond their maximum spec at 16 VDC. A 14.7 V / 0.5 kOhm = 29.4 mA current will flow through the LEDs when on, which is higher than the 25 mA spec. The LEDs will not immediately break because of this, they will light brighter and eventually burn out (long after your relays have burned out).








Programming



7/2012: Someone has told me that my code is "taking the current heading to the destination way point (stored in variable nSteerBearing) and treating it as the error in the current bearing. The code in question is after the comment 'time to do something'. What I think is really needed is the difference between the current bearing and the bearing to the destination. I am planning to get the current bearing from the GPVTG sentence, compare it to field 11 and steer appropriately.".
[He could well be right; I don't know.]

8/2012 received this:
Ian is 100% correct about the VTG changes to the auto-pilot. The program is reading the Xtrack error as a steering reference. The GPRMB sentence alone is not enough information to steer correctly as the field "11" only gives the starting point bearing. Also the Steering bearing minimum is not referenced to anything so it will revert to "0", meaning the program won't steer at all if the starting bearing is in the degree range stated in the steering bearing minimum. I'm presently trying to get the program modified to read the VTG sentence and compare the " true track made good" to the GPRMB "true bearing to destination".
then:
Scratch that, on my unit the RMC gives the true course, that'll be what I have to use.



//--------------------------------------------------------------------------
// GPS-auto-pilot program on PIC-IO-18 (PIC-IO-A) board
// with PIC16F628A chip installed.

// Connect GPS at 9600 baud to DB9 connector on PIC-IO-18 board.
// Connect relays 1 and 2 to auto-pilot "back-end" (some
// electrically-controlled steering mechanism) in your boat.
// Create a waypoint in the GPS.
// Tell GPS to "go to" the waypoint.
// Auto-pilot should steer to the waypoint.

// Normal operation: at power-up, board's LED9 should turn on for 2.5 secs,
// then should blink on and off as each NMEA sentence is received from GPS.
// While GPS is in goto-waypoint mode, relay 1 or 2 should turn on to
// steer boat back onto course as needed.
// So:
// - If LED9 doesn't turn on at startup, maybe board doesn't have power,
//		or auto-pilot program hasn't been downloaded into board, or board
//		or chip is bad.
// - If LED9 doesn't blink while GPS is on, maybe wiring from GPS to board
//		is wrong, or GPS is not producing NMEA output, or NMEA baud rate
//		doesn't match what the program is expecting.
// - If relays don't come on as boat goes off-course, maybe GPS is not
//		in goto-waypoint mode, or boat is within arrived-at-waypoint distance,
//		or GPS does not produce NMEA $GPRMB sentence that the program is
//		expecting.
// - If relay comes on but boat course does not change, maybe wiring from
//		board relays to your auto-pilot back-end is wrong, or your back-end
//		is not working.
// - If auto-pilot steering sensitivity or aggressiveness is wrong, change
//		STEERING_ON_DURATION, STEERING_AFTER_DURATION, and/or
//		STEERING_BEARING_MINIMUM values, rebuild program, download it to
//		board, and test again.

// Note: program requires just over 1K of program space in processor,
// with LITE compiler.  Supposedly PRO compiler would require half that.
// Could remove some error-checking code to get under the 1K limit.

//--------------------------------------------------------------------------

#include <pic.h>
#include <pic16f62xa.h>

__CONFIG(UNPROTECT & LVPDIS & BORDIS & MCLREN & PWRTDIS & WDTDIS & HS); // 0x3F2A
/* for PIC-IO-18 board, oscillator configuration == HS */

/*
for PIC-IO-18 board:
output: RB5 = status LED9
inputs: RA4 = LED1/IN1, RB0 = LED2/IN2, RB3 = LED3/IN3, RB4 = LED4/IN4
outputs/relays: RA3 = LED5/RELAY1, RA2 = LED6/RELAY2, RA1 = LED7/RELAY3, RA0 = LED8/RELAY4

for auto-pilot application:
RB5/LED9 toggled when character received
RA3/LED5/RELAY1 turned on to steer left
RA2/LED6/RELAY2 turned on to steer right

For my old auto-pilot back-end, need to apply line positive to either
R or L terminal of back-end to effect steering right or left.
Although my particular back-end would tolerate it, want to avoid any possibility
of turning on BOTH at same time (software error could turn on both relays at same time).
In some systems, maybe this could short power to ground and blow something up.
So do wiring to make this software error harmless:
- connect line positive to center post of relay 1.
- connect "on" post of relay 1 to "L".
- connect "off" post of relay 1 to center post of relay 2.
- connect "on" post of relay 2 to "R".
Now relay 2 will not get any connection to positive unless relay 1 is off.
No possibility of both "L" and "R" getting connected to positive even
if both relays get turned on at same time.
*/

//--------------------------------------------------------------------------

// do RS232 9600 baud, 8 bits
#define BAUD 9600
#define FOSC 20000000L
#define DIVIDER ((int)(FOSC/(16UL * BAUD) -1))// for >= 9600 baud
//#define DIVIDER ((int)(FOSC/(64UL * BAUD) -1))// for < 9600 baud

// signal permanent errors by setting relays 3 and 4 ?
// if 1, then don't wire anything to relays 3 and 4 !
#define SIGNAL_PERMANENT_ERRORS_ON_RELAYS_3_AND_4		1

// tweak these parameters to make auto-pilot steering more or less aggressive
// duration of steering in 10 msec units (actually, seems to be more like 12.5 msec units)
#define STEERING_ON_DURATION		250
// duration of after-steering wait in 10 msec units (actually, seems to be more like 12.5 msec units)
// (not sure of maximum value before overflow; tested up to 6000
#define STEERING_AFTER_DURATION		1500
// don't try to correct until GPS says we're at least this many degrees off course
// includes digit after decimal point, so divide by 10 to get degrees from on-track
#define STEERING_BEARING_MINIMUM	50



// sentence from GPS will look like:
// $GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*0BCRLF
int nNextField = 0;		// NMEA field num; fields are comma-separated
int nNextPosInField = 0;	// char num within field
int nXTError = 0;		// GPS error "n.nn" converted to integer nnn
int nSteerBearing = 0;	// GPS error "n.nn" converted to integer nnn
char cSteerDir = ' ';

// If count>0, we are starting up. Stop when it gets down to 0.
long lStartingCount = 0;
// If count>0, we are steering. Stop steering when it gets down to 0.
long lSteeringCount = 0;
// If count>0, we are waiting after steering. Stop waiting when down to 0.
long lPostSteeringCount = 0;

bit bPermanentFatalError = 0;

bit bLED9On = 0;

//--------------------------------------------------------------------------

main(void)
{
TRISA = 0;		// all port A bits output
TRISB = 0;		// all port B bits output
PORTA = 0x00;	// turn all outputs off
PORTB = 0x00;	// turn all outputs off

lStartingCount = 200;
lSteeringCount = 0;		// not steering
lPostSteeringCount = 0;
bLED9On = 0;

// Timer 0 is an 8-bit counter that interrupts
// when it overflows from 0xFF to 0x00.
// There is an 8-bit "prescaler", which counts
// how many cycles (0-256) to ignore before incrementing
// the counter each time.
// This prescaler is shared with the watchdog timer.
OPTION = 0b0111; // Timer 0 prescale by 1:256
T0CS = 0; // Timer 0 increments on instruction clock
T0IE = 1; // Enable interrupt on TMR0 overflow

// initialize communications
TRISB = 0x06; // RB1 and RB2 used by USART
SPBRG = DIVIDER;// set baud rate
RCSTA = 0x80; // serial port enable, 8-bit
TXSTA = 0x04; // 8-bit, no-xmit, async, high-speed
RCIE = 1; // enable recv interrupts
TXIE = 0; // no xmit interrupts
CREN = 1; // continuous recv enable

RB5 = 1;	 // LED9 on while starting

PEIE = 1; // enable peripheral ints; needed for USART
GIE = 1; // Global interrupt enable

for(;;)
	NOP(); // do nothing forever; action happens in interrupt routine
}

//--------------------------------------------------------------------------

// interrupt function - the name is unimportant
static void interrupt
isr(void)
{
char cInput = ' ';

// timer 0 overflow ?
if (T0IF) {
	if (bPermanentFatalError) {
		T0IF = 0; // clear interrupt flag, ready for next
		return;
	}
	if (lStartingCount > 0) {
		lStartingCount--;
		if (lStartingCount <= 0)
			RB5 = 0;	 // LED9 off after starting
	} else if (lSteeringCount > 0) {
		lSteeringCount--;
		if (lSteeringCount <= 0) {
			// stop steering
			lSteeringCount = 0;
			// don't steer left
			//RA3 = 0; // LED5/RELAY1
			//RA2 = 0; // LED6/RELAY2
			// don't steer right
			//RA1 = 0; // LED7/RELAY3
			//RA0 = 0; // LED8/RELAY4
			//PORTA ^= 0xF0;		// turn off RA0/RA1/RA2/RA3 bits
			PORTA = 0x00;		// turn off RA0/RA1/RA2/RA3 bits
			lPostSteeringCount = STEERING_AFTER_DURATION;
		}
	} else if (lPostSteeringCount > 0) {
		lPostSteeringCount--;
	}
	T0IF = 0; // clear interrupt flag, ready for next
}

// received char ?
else if (RCIF) {
	if (bPermanentFatalError) {
		cInput = RCREG;
		RCIF = 0;
		return;
	}
	if (FERR) {
		// framing error
		cInput = RCREG; 	// clears FERR
		// no way to tell user about problem !!!
		if (lStartingCount == 0) {
			lSteeringCount = 0;
			lPostSteeringCount = 0;
#if SIGNAL_PERMANENT_ERRORS_ON_RELAYS_3_AND_4
			// to signal problem
			PORTA = 0x03;		// turn on RA0/RA1 bits; RELAY4/RELAY3
			bPermanentFatalError = 1;
#endif
		}
		// ignore any partial line recved
		nNextField = 0;
		nNextPosInField = 0;
		RCIF = 0; 		// clear interrupt flag, ready for next char
		return;
	}

	cInput = RCREG;

	if ((lStartingCount == 0) && (cInput == '$')) {
		// toggle LED9 to show that a char was received
		bLED9On = !bLED9On;
		RB5 = bLED9On; // LED9
	}

	// while starting or steering or waiting after steering, ignore GPS output
	if ((lStartingCount > 0) || (lSteeringCount > 0) || (lPostSteeringCount > 0)) {
		cInput = ' ';
		nNextField = 0;
		nNextPosInField = 0;
	}

	// sentence from GPS will look like:
	// $GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*0BCRLF
	//				 ^----- steer left or right (field 3)
	//			^^^^----- cross-track error (field 2)
	//				(field 11) true bearing to dest ------^^^^^
	//					(field 13) 'A' if within arrival circle ------^
	// 01234567890123456789012345678901234567890123456789012345
	if (nNextField == 0) {
		const char sStart[10] = "$GPRMB";
		if (cInput == sStart[nNextPosInField])
			nNextPosInField++;
		else if (cInput == ',') {
			nNextField++;
			nNextPosInField = 0;
		} else
			// unexpected char; give up
			// nNextField is 0 already
			nNextPosInField = 0;
	}
	else if (nNextField == 1) {
		// Status field
		if ((nNextPosInField == 0) && (cInput == 'A')) {
			// okay
			nNextPosInField++;
		} else if ((nNextPosInField == 1) && (cInput == ',')) {
			nXTError = 0;
			nNextField++;
			nNextPosInField = 0;
		} else {
			// unexpected char; give up
			nNextField = 0;
			nNextPosInField = 0;
		}
	}
	else if (nNextField == 2) {
		// XTK error magnitude
		if ((cInput >= '0') && (cInput <= '9')) {
			nXTError = (nXTError * 10) + (cInput - '0');
		} else if (cInput == '.') {
			// do nothing
		} else if (cInput == ',') {
			nNextField++;
			nNextPosInField = 0;
		} else {
			// unexpected char; give up
			nNextField = 0;
			nNextPosInField = 0;
		}
	}
	else if (nNextField == 3) {
		// steering direction
		if ((nNextPosInField == 0) && ((cInput == 'L') || (cInput == 'R'))) {
			cSteerDir = cInput;
			nNextPosInField++;
		} else if ((nNextPosInField == 1) && (cInput == ',')) {
			nSteerBearing = 0;
			nNextField++;
			nNextPosInField = 0;
		} else {
			// unexpected char; give up
			nNextField = 0;
			nNextPosInField = 0;
		}
	}
	else if (nNextField == 11) {
		// steering bearing
		if ((cInput >= '0') && (cInput <= '9')) {
			nSteerBearing = (nSteerBearing * 10) + (cInput - '0');
		} else if (cInput == '.') {
			// skip decimal point
		} else if (cInput == ',') {
			nNextField++;
			nNextPosInField = 0;
		} else {
			// unexpected char; give up
			nNextField = 0;
			nNextPosInField = 0;
		}
	}
	else if (nNextField > 11) {
		// time to do something
		//if (nXTError < 10)
		//	cSteerDir = 'N';	// don't steer
		if (nSteerBearing < STEERING_BEARING_MINIMUM)
			cSteerDir = 'N';	// don't steer
		// now start steering
		if (cSteerDir == 'R') {
			// error to right, steer left
			RA3 = 1; // LED5/RELAY1
			// PORTA = 0x0C;		// turn on RA2/RA3 bits
		} else if (cSteerDir == 'L') {
			// error to left, steer right
			RA2 = 1; // LED6/RELAY2
			// RA1 = 1; // LED7/RELAY3
			// RA0 = 1; // LED8/RELAY4
			// PORTA = 0x03;		// turn on RA0/RA1 bits; RELAY4/RELAY3
		} else {
			// don't steer
		}
		if ((cSteerDir == 'L') || (cSteerDir == 'R')) {
			lSteeringCount = STEERING_ON_DURATION;
			lPostSteeringCount = 0;
		}
		// ready to start next sentence
		nNextField = 0;
		nNextPosInField = 0;
	}
	else {
		// process char in field we don't care about
		if (cInput == ',') {
			nNextField++;
			nNextPosInField = 0;
		} else {
			// do nothing; throw char away
		}
	}

	if (OERR) {
		// overrun error
		CREN = 0;
#if SIGNAL_PERMANENT_ERRORS_ON_RELAYS_3_AND_4
		// to signal problem
		PORTA = 0x01;		// turn on RA0; RELAY4
		bPermanentFatalError = 1;
#endif
		lSteeringCount = 0;
		lPostSteeringCount = 0;
		// ignore any partial line recved
		nNextField = 0;
		nNextPosInField = 0;
		CREN = 1;
	}

	RCIF = 0; // clear interrupt flag, ready for next char
}

// unexpected interrupt
else {
	// no way to tell user about problem !!!
	if (!bPermanentFatalError) {
		lSteeringCount = 0;
		lPostSteeringCount = 0;
#if SIGNAL_PERMANENT_ERRORS_ON_RELAYS_3_AND_4
		// to signal problem
		PORTA = 0x02;		// turn on RA1; RELAY3
		bPermanentFatalError = 1;
#endif
	}
}

}

//--------------------------------------------------------------------------