{
========================================================================================

				 O B S   M A S T E R   C L O C K   S Y N C H R O N I Z A T I O N


Program     :	OBSSYNC.PAS
Description :	Based on GPS Pulse-per-second (PPS) and NMEA formatted
							serial input (GPGGA sentence), the program issues OBS
              Master Clock synchronizing pulses. The pattern is as
              follows:



																 _____       _____
							|                 |     |     |     |
							|_________________|     |_____|     |_____

							0     5    10    15    20    25    30   [second of minute]




							GPS PPS pulse assumed to be active high, of TTL level,
							and of min 1 ms duration. Leading edge marks second
							boundary.

                     ___
                    |   |
              ______|   |_____

                    ^
                    |___ Second boundary



              Program is tested with Garmin GPS mod 35-HVS.


PC ports:     LPT1  :  Pin 10 (ACK~) = Input, PPS from GPS.
                                       The positive edge of a signal
                                       on this pin can be used to generate
                                       an interrupt (assumed INT7 on the
                                       PC PIC-1 (Programmable Interrupt Controller).
                       Pin 2 (D0)    = Output, OBS Master Clock calibration
                                       signal.
                       Pin 18        = GND

              COM   :  Program uses COM2. Use only RXD-pin and GND.



NMEA sentence
used:         GPGGA = Global Positioning System Fix Data
              $GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,M,<10>,M,<11>,<12>*hh<CR><LF>
                <1> UTC time of position fix, hhmmss format
                <2> Latitude, ddmm.mmmm format (leading zeros will be transmitted)
                <3> Latitude hemisphere, N or S
                <4> Longitude, dddmm.mmmm format (leading zeros will be transmitted)
                <5> Longitude hemisphere, E or W
                <6> GPS quality indication,
                    0 = fix not available,
                    1 = Non-differential GPS fix available,
                    2 = Differential GPS (DGPS) fix available
                <7> Number of satellites in use, 00 to 12 (leading zeros will be transmitted)
                <8> Horizontal dilution of precision, 0.5 to 99.9
                <9> Antenna height above/below mean sea level, -9999.9 to 99999.9 meters
               <10> Geoidal height, -999.9 to 9999.9 meters
               <11> Differential GPS (RTCM SC-104) data age, number of seconds since last
                    valid RTCM transmission (null if non-DGPS)
               <12> Differential Reference Station ID, 0000 to 1023 (leading zeros transmitted,
                    null if non-DGPS)



Copyright :	University of Bergen, Norway
						Institute of Solid Earth Physics
Language  :	Turbo Pascal 7.0
Tabs      : 2



History   : Rev.  Date         By  Description
						-------------------------------------------------------
						0.1   24 aug 2000  OM  Test version


========================================================================================
}

PROGRAM OBS_Sync;
Uses Crt, DOS;




{
=================================================================

		C o n s t a n t s

=================================================================
}



CONST

	NMEA_FIELD_SEPERATOR = ',';
	NMEA_FAILURE = '|';
	CR = #$0D;
	LF = #$0A;
	COM1_BIOS_OFFSET	= $400;
	COM2_BIOS_OFFSET	= $402;
  LPT1_BIOS_OFFSET  = $408;
	LINE_STATUS_REG		= 05;		{COM UART Line Status Register offset from base adr.}
	LINE_CONTROL_REG  = 03;		{COM UART Line Control Register offset from base adr.}
	COM_1			= $0000;
	COM_2			= $0001;
	Baud9600		= $E0;
	Baud4800		= $C0;
	NoParity		= $00;
	OddParity		= $08;
	EvenParity		= $18;
	OneStopBit		= $00;
	TwoStopBits 	= $04;
	Len7Bits		= $02;
	Len8Bits		= $03;


{
=================================================================

		V a r i a b l e s

=================================================================
}


VAR
	OldExitProc 		: Pointer;
	OldLpt1_Vector	: Pointer;
	LPT1_data				: Byte;
	LPT1_data_adr		: Word;
	LPT1_ctrl_adr		: Word;
	COM_base_adr		: Word;
	UartLineStatus	: Byte;
	UartDataIn			: Byte;
	Seconds         : Byte;
	nmea : string;
	Gps_Nmea				: String;
	No_of_satellites: string;
	PPS_occured : Boolean;

{
=================================================================

		P r o c e d u r e s   /   f u n c t i o n s

=================================================================
}


{NMEA parsing function.
 If target field is empty, e.g. when targeting field no. 1 in
 'GPGGA,,,', function will return empty string.
 An error code is produced if:
 -  If NMEA code does not match
 -  If there are not sufficient NMEA separators
 -  If first char in NMEA sentence is not dollar sign
 NMEA code is e.g. 'GPGGA'.
 }


FUNCTION NMEA_parsing(NMEA_code, NMEA_sentence : string;
				 NMEA_field_no : integer) : string;

VAR
	NMEA_field_counter : integer;
	NMEA_field_found : Boolean;
	Match_found : Boolean;
	k : Integer;
	s : string;

BEGIN

	{-- Variable initialization}

	NMEA_field_counter := 0;
	Match_found := TRUE;
	NMEA_field_found:= FALSE;
	NMEA_parsing := NMEA_FAILURE;


	{-- First check that NMEA sentence starts with dollar sign.
			Then ensure that sentence is longer than matching pattern
			plus 2 extra characters (dollar sign and one comma) ---}

	If (NMEA_sentence[1] <> '$') then Match_found := FALSE;
	If (ord(NMEA_sentence[0]) <= (ord(NMEA_code[0])+2)) then Match_found := FALSE;


	{-- Pattern matching check }

	For k:= 1 to length(NMEA_code) DO
	begin
		IF NMEA_code[k] <> NMEA_sentence[k+1] then
		begin
			Match_found := FALSE;
			break;
		end;
	end;


	{If match, locate field ..}

	IF Match_found then
	Begin
		For k:=1 to length(NMEA_sentence) DO
		begin
				If (NMEA_sentence[k] = NMEA_FIELD_SEPERATOR) then INC(NMEA_field_counter);
				If (NMEA_field_counter = NMEA_field_no) then
				Begin
					 NMEA_field_found:= TRUE;
					 break;
				end;
		end;


	{If field found, extract content of this field }

		IF NMEA_field_found then
		begin
			s := '';
			INC(k);
			While ((NMEA_sentence[k] <> NMEA_FIELD_SEPERATOR) AND (k <= length(NMEA_sentence))) DO
			begin
				s := s + NMEA_sentence[k];
				INC(k);
			end;
			NMEA_parsing := s;
		end;

	end;


END;





FUNCTION BCD2Word(Value : Byte) : Word;

VAR
	W	: Word;

BEGIN
	W			:= ((Value AND $F0) shr 4) * 10;	{Hi part}
	W	 		:= W + (Value AND $0F);				{Lo part}
	BCD2Word	:= W;
END;



FUNCTION Hex2Str(Value : Byte) : String;

	FUNCTION Digit( n : Byte ) : Char;
	{ For 0 <= n <= 15 }

	BEGIN
		 IF n < 10 THEN
			Digit := Chr( ord('0') + n )
		 ELSE
			Digit := Chr( ord('A') + ( n - 10 ) )
	END;  { Digit }


BEGIN
	Hex2Str := Digit(Value DIV 16)+Digit(Value MOD 16);
END;



FUNCTION BCD2Str(Value : Byte) : String;

VAR
	Kladd	: Byte;
	s1,s2	: String;

BEGIN
	Kladd := (Value AND $F0) shr 4;
	STR(Kladd,s1);
	Kladd := Value AND $0F;
	STR(Kladd,s2);
	BCD2Str := s1+s2;
END;




PROCEDURE COM_Init(ComPort: Word; Rate, Parity, Stop, LenBits: Byte);
VAR								{NOTE: RTS strapped to CTS in conn!!}
	Reg	: Registers;
 BEGIN
 WITH Reg DO
	BEGIN
		ah := $00;
		al := Rate+Parity+Stop+LenBits;
		dx := ComPort;
		INTR($14, Reg);
		{Writeln('ah = ', BCD2Str(reg.ah), '  al = ', BCD2Str(reg.al));}
	END;
 END;






 PROCEDURE HardwareSetup;

 Var
	b : byte;
 BEGIN

	{----- Look in BIOS equipment list to locate
				 LPT1 data port address. LPT1 Control port is assumed to
				 be positioned two units higher in the address space. ----}

	LPT1_data_adr := memw[0:LPT1_BIOS_OFFSET];
	LPT1_ctrl_adr := LPT1_data_adr + 2;
	writeln('LPT1 data adr = ', Hex2Str(hi(LPT1_data_adr))+Hex2Str(lo(LPT1_data_adr))+'H');

	If (LPT1_data_adr = 0) then
	begin
		Writeln('LPT1 port not found in BIOS equipment list.');
		Writeln('Check that LPT1 is enabled in BIOS setup.');
		Writeln('Press any key to quit.');
	end;



	{----- Look in BIOS equipment list to locate COM port address. ----}

	COM_base_adr := memw[0:COM2_BIOS_OFFSET];  {Change to use COM1}
	writeln('COM base adr  = ', Hex2Str(hi(COM_base_adr))+Hex2Str(lo(COM_base_adr))+'H');

	If (COM_base_adr = 0) then
	begin
		Writeln('COM port not found in BIOS equipment list.');
		Writeln('Check that COM1 is enabled in BIOS setup.');
		Writeln('Press any key to quit.');
	end;


	COM_Init(COM_1, Baud9600, NoParity, OneStopBit, Len8Bits);
	COM_Init(COM_2, Baud9600, NoParity, OneStopBit, Len8Bits);
	PORT[COM_base_adr + LINE_CONTROL_REG]	:=	PORT[COM_base_adr + LINE_CONTROL_REG] AND $7F;
	b	:=	PORT[COM_base_adr+LINE_STATUS_REG];	{Read the Line Status Register (LSR) to reset any errors it indicates}
	b	:=	PORT[COM_base_adr];	{Read the receive buffer in case it contains a char}



 END;




{
==================================================================

			 I n t e r r u p t   /   e x i t   r o u t i n e s

==================================================================
}



PROCEDURE Lpt1_Interrupt; INTERRUPT;
VAR
	Dummy : Word;

BEGIN

	INC(Seconds);
	IF (Seconds = 60) then Seconds := 0;

	IF (((Seconds MOD 5) = 0) AND NOT(Seconds = 5) AND NOT(Seconds=10)) THEN
		 BEGIN
			 LPT1_data := LPT1_data XOR 1;   {Flip least sign. bit = D0}
			 port[LPT1_data_adr] := LPT1_data;
		 END;


	PPS_occured := TRUE;


	PORT[$20]	:= $20;			{Issue non-specific EOI to 8259 interrupt Controller}



END;





{$F+}
PROCEDURE MyExitProc;
BEGIN


	ExitProc := OldExitProc; { Restore exit procedure address }
	SetIntVec($0f, OldLpt1_Vector);

	SOUND(1000);
	Delay(100);
	NOSOUND;

end; { MyExitProc }
{$F-}





PROCEDURE Init_InterruptAndExitVectors;

var b: Byte;

BEGIN

	OldExitProc := ExitProc;					{ Save previous exit proc }
	ExitProc 	:= @MyExitProc;					{ Insert our exit proc in chain }

	GetIntVec($0f, OldLpt1_Vector);
	SetIntVec($0f, @Lpt1_Interrupt);


	b := port[$21];       {Setup PC's PIC-1 (Interrupt Controller): Enable INT 7}
	write('Old Int mask: ', hex2str(b));
	b := b AND $7f;
	port[$21] := b;
	b := port[$21];
	writeln('  New Int mask: ', hex2str(b));

	port[LPT1_ctrl_adr]	:= $10;  {0001 0000  Bit 4 = 1: Enable ack interrupt}

	PORT[$20]	:= $20;			{Issue non-specific end-of-interrupt to 8259 PIC-1interrupt Controller}


END;





{-------------------------------------------------------}

{------------   M A I N    P R O G R A M   -------------}

{-------------------------------------------------------}


Var
	b : Byte;
	Quit : Boolean;
	d : string;
	dd: string;
	Nmea_Error_Counter : LongInt;
	I, Code: Integer;



 BEGIN


	ClrScr;
	WriteLn('***********************************************************');
	WriteLn('**  OBS Master clock syncronization program.');
	WriteLn('**  Version 0.1, 24 Aug. 2000 (OM)');
	Writeln('**');
	WriteLn('**  Based on GPS Pulse-per-second (PPS) and NMEA formatted');
	WriteLn('**  serial input, the program issues OBS Master Clock');
	WriteLn('**  synchronizing pulses. NOTE: Program resets internal');
  WriteLn('**  second counter at the start of each minute. Output');
  WriteLn('**  is not valid until PPS- and UTC seconds are equal.');
	WriteLn('**  Program must run in DOS mode.');
	WriteLn('**  Institute of Solid Earth Physics');
	WriteLn('**  University of Bergen, NORWAY');
	WriteLn('***********************************************************');
  Writeln;


	Quit               := FALSE;
	PPS_occured        := FALSE;
  seconds            := 0;
	Gps_Nmea           := '';
	Nmea_Error_Counter := 0;

	HardwareSetup;
	Init_InterruptAndExitVectors;




{--- This is the main loop. Runs until any key is pressed. ---}

REPEAT


  {--- Did PPS from GPS generate interrupt? If so, PPS_occured flag was set.}

	IF PPS_occured then
	begin
		PPS_occured := FALSE;
		gotoxy(1,22);
		write('PPS interrupt! Internal second counter          = ', Seconds,' ');
	end;


  {--- Reception of serial characters is based on polling the UART Data Ready bit
       in the Line Status Register.}

	UartLineStatus := port[COM_base_adr+LINE_STATUS_REG] AND 1; {Keep Data Ready bit}

	IF (UartLineStatus <> 0) then
		begin                                     {Character present, get it, store it ..}
			UartDataIn := port[COM_base_adr];
			Gps_Nmea := Gps_Nmea + Chr(UartDataIn);
			IF (Chr(UartDataIn) = LF) then
			begin                                   {End of Line character, start parsing.}
				d := NMEA_parsing('GPGGA', Gps_Nmea, 1);	{Field 1 = time}
				No_of_satellites := NMEA_parsing('GPGGA', Gps_Nmea, 7);	{Field 7 = No of satellites in use}
				Gps_Nmea := '';
				IF ((d = NMEA_FAILURE) OR (No_of_satellites=NMEA_FAILURE)) then
					begin
						INC(Nmea_Error_Counter);
						gotoxy(1,21);
						write('Error in GPS NMEA sentence, error count: ', Nmea_Error_Counter);
					end
				else
					begin
						gotoxy(1,23);
						sound(1000);
						write('UTC: ',d, '  No. of satellites = ', No_of_satellites,'  ');
						dd := 'xx';
						dd[1] := d[5]; dd[2] := d[6];
						Val(dd, I, Code);
						{ Error during conversion to integer? }
						if code <> 0 then
							Write('Error at position: ', Code)
						else
							begin
								If (I=0) then Seconds := 0;
								Write('UTC second = ', I,' ');
							end;

						delay(1);
						NoSound;
					end;
			end;

		end;


UNTIL KeyPressed;


end.