{ ======================================================================================== 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.