GPS Data receiver using X2's background serial data reception

#1
During the development of Little Ben, the need arose for a clock timer that did not require adjusting or periodical replacement of a backup battery. Little Ben 'clock' was to be installed in a public recreation park and needed to thrive on neglect and withstand power outages while maintaining accurate time.

I had a couple of surplus GPS data receiver modules, originally intended for use in a car navigation device. A check of the datasheet indicated that the module required 3.3 volts at 45mA and the default data output should be 9600 baud. On-line research showed that the data packet with a "$GPGGA" header would probably give me that data I wanted for a clock.

I initially placed the GPS receiver on a window sill, with an extension cable connected to an AXE027 into my computer. The PICAXE terminal showed the data at 9600 baud was garbled. The default output format for my module idled high. Sequences of data packets were being sent every second.

The attached code demonstrates a PICAXE 28X2 GPS data receiver, using the background receive mode for its hardware UART 'hSerial' port. The PICAXE's background timer is also used, to provide a backup should GPS data be corrupted.

Hardware for the demonstration consisted of a PICAXE 28X2 running off a (LM7805-based) 5v supply. This is fed into a LD1117AV33 low dropout 3.3v regulator, used to supply the GPS module. Data from the GPS receiver is fed directly into PICAXE hSerIn/Pin C.7/Leg 18. A LED & current limiting resistor are connected to pin A.1/Leg 3, used as a digital output.

The demonstration software receives data from the GPS module, which needs reasonable access to satellite data (Eg. placed near a window - my home has double brick walls and a steel roof). The PICAXE firmware receives data from the hardware UART and logs it sequentially in the scratchpad RAM, used as a circular buffer. Subroutine hReceive searches the logged data for a "$GPGGA" header (in subroutine Check_GPS_Header) and waits for the entire packet to be received before building the checksum in subroutine hRecCheckSum. If the rebuilt checksum matches the received checksum, the master time variables are overwritten in subroutine hReadGPSData

The time is output to the PICAXE Terminal window at 19200 baud every 10 seconds. If "#Define Show_Packet_Diags" is specified, every validly received "$GPGGA" packet is logged as well. A working version would not have "#Define Show_Packet_Diags" specified. Note that "#Define Show_Packet_Diags" places a considerable load on the PICAXE and it does not always receive and validate every "$GPGGA" data packet. If the packet's checksum fails validation, the packet is discarded. When this happens, the background timer's 1-second interrupt keeps time, with minutes and hours incrementing where appropriate.

If you are adding your own code to the main loop of the program, be aware that blocking commands could interfere with the operation of background serial data reception. Blocking commands could also prevent interrupts, stopping the software backup clock from 'ticking' every second.

I have included a lot of comments to help explain the operation of the code. As a result, the code is quite large and has to occupy two posts: Declarations/Initialisation/Main_Loop/Interrupts and All Subroutine Code.
 
#2
Code part 1: Declarations/Initialisation/Main_Loop/Interrupts
Code:
'GPS Signal receiver decoder by inglewoodpete
Symbol Version = 4  '09-Jul-2018  952 bytes Added checksum handler
'
Symbol Major = 0    ' Major revision ID
'
' Note that when Show_Packet_Diags is defined, PICAXE will not process every GPS time packet
#Define Show_Packet_Diags
'
#PICAXE 28X2
'#COM xx
#Terminal 19200
'
' **** Hardware Pins Definitions - i prefix for inputs; o for outputs; b for bothway pins
'
Symbol oLED          = A.1       ' 3 A.1 After bootup, Toggles on every vaid GPS data packet received
Symbol oLEDValue     = outpinA.1 ' 3 A.1
'
Symbol ohSerOut      = C.6       ' 17 Background Serial Async Out
Symbol ihSerIn       = C.7       ' 18 Background Serial Async In
'
' **** Variables - t prefix: bit variable; b: byte; w: word; r: other RAM; s: scratchpad; e: EEPROM
'
Symbol tMismatch     = bit0
Symbol tNoGPSDataYet = bit1   'b0, w0  Set to True at bootup until first valid GPS packet received
Symbol tRecError     = bit2   'b0, w0  Used to flag bad checksum byte rec'd Eg 2F is ok; DJ would be bad
Symbol tGoodPacket   = bit3   'b0, w0  GPGGA packet with checksum confirmed correct
'
Symbol bSeconds	     = b2     'w1      System time (regularly updated by GPS Data)
Symbol bMinutes      = b3 	  'w1      System time (regularly updated by GPS Data)
Symbol bHours12      = b4     'w2      System time (regularly updated by GPS Data) Hours in 12-hour format
Symbol bHours24      = b5     'w2      Hours in 24-hour format, taken from GPS data
Symbol bPtrMem       = b6     'w3      Copy of Ptr variable for later reuse
Symbol bEEPROMPtr    = b7     'w3      Used to validate GPS data packet header
'
Symbol tmpSeconds    = b20	  'w10    As received in valid GPS data packet
Symbol tmpMinutes    = b21	  'w10
Symbol tmpHours      = b22	  'w11
Symbol bLoop         = b23    'w11
Symbol bLastSecond   = b24    'w12  The last second where time was printed
Symbol bAMPM         = b25    'w12
Symbol bTemp         = b26    'w13
Symbol bData         = b27    'w13
Symbol bPktLen       = b28    'w14
Symbol bCalcCSum     = b29    'w14  Locally reconstructed Checksum
Symbol bRecdCSum     = b30    'w15  Checksum received in GPS data packet
Symbol bNibble       = b31    'w15   RecHex
Symbol bError        = b32    'w16   RecHex
Symbol bByteToShow   = b33    'w16  Used to transfer a value to ShowHex
'
Symbol wBufferedDataLen = w21 'b42/43  Byte count for current GPS data packet
Symbol wPktStartPtr  = w22    'b44/45  Points to last $ sign
Symbol wPktDataPtr   = w24    'b46/47  Points to Start-of-Data, after the "$GPGGA," header
'
' **** Constants - Prefix = c; msk (mask); flg (flag)
'
Symbol False         = 0
Symbol True          = 1
'
Symbol cTimeOffset   = 8            'Western Australia is 8 hours ahead of UTC
'
Symbol mskSerInRange = %1111111111  'Bytes 0-1023 (10-bit mask)
'
Symbol mskLoNibble   = %00001111
Symbol mskHiNibble   = %11110000
'Interrupt masks
Symbol mskTmrAndSer  = %10100000
Symbol flgTmrAndSer  = %10100000
'Timer constants
Symbol tmrIntOn1stTick = 65535   'Interrupt to be caused by roll over on first major tick
Symbol cOneMinute    = 60        'Seconds
Symbol cOneHour      = 60        'Minutes, not seconds
'
'Pause periods at 16MHz (PICAXE X2 Models)
Symbol c100mS        =  200      ' 100mS
Symbol c1S           = 2000      '1000mS
'
' **** Scratchpad - Prefix = s (28X2 - Bytes 0 to 1023d)
'
Symbol sSerInBuffStart  = 0
'
' **** EEPROM  - Prefix = e (256 Bytes - 0 to 255d)
'
Symbol eGPSHeader = 0
EEPROM eGPSHeader, ("$GPGGA,")      'Header for data packet containing time data
Symbol eEOData = 6
'
' ***** C O D E *********************************************************************
'
Init: SetFreq m16
      Pause c1S
      '
      For bLoop =  1 to 16          '8 flashes
         Pause c100mS               'Put Pause first to allow SetFreq to settle
         Toggle oLED
      Next bLoop
      SerTxd (CR, LF, CR, LF, "Booted: GPS Data Receiver v", #Major, ".", #Version, CR, LF)
      '
		'Initialise serial Downlink for GPS comms
		Ptr = sSerInBuffStart
      hSerPtr = sSerInBuffStart
		hSerSetup B9600_16, %00001  '9600 baud @ 16MHz, Background, no inversion
      '
      'Background timer provides reasonably accurate backup timekeeping if GPS data is intermittent
      'Start the background timer (runs continuously)
      Timer = tmrIntOn1stTick
      SetTimer t1S_16            'Expires after 1/8 second @ 16 MHz
		Flags = 0						'Reset serial reception flag
      SetIntFlags Or flgTmrAndSer, mskTmrAndSer 'Set timer 0 or hSerial to interrupt
      '
      tNoGPSDataYet = True       'Log GPS data until first valid packet received
      SerTxd ("Initialisation Complete: Enter Main Loop", CR, LF)
      '
      ' ------ MAIN LOOP ---------------
      '
      Do
         'Insert (add) your own code here.  Avoid blocking commands!
         '
         GoSub hReceive
         'The following code displays the time once every 10 seconds
         If bLastSecond <> bSeconds Then     'Don't log multiple times within one second
            tmpSeconds = bSeconds // 10      'Get the remainder (returns a value 0 to 9)
            If tmpSeconds = 0 Then           'Log once every 10 seconds
               GoSub ShowTime                'Show the current time
               bLastSecond = bSeconds        'For comparison, next loop
            EndIf
         EndIf
      Loop
'
' ************************************************************
'  Interrupt handler
' ************************************************************
'  
Interrupt:If hSerInFlag = True then
				hSerInFlag = False
			 EndIf
          If TOFlag = True Then
            TOFlag = False                         'Reset (clear) the flag first
            Inc bSeconds
            If bSeconds = cOneMinute Then          '/--- System Timer
               bSeconds = 0                        '|
               Inc bMinutes                        '|
               If bMinutes = cOneHour Then         '|
                  bMinutes = 0                     '|
                  Inc bHours12                     '|
                  If bHours12 = 13 Then            '|
                     bHours12 = 1                  '|
                     bAMPM = " "                   '| Don't know if it is AM or PM
                  EndIf                            '|
               EndIf                               '|
            EndIf                                  '|
            '
            Timer = tmrIntOn1stTick                   'Then reset the timer
          EndIf    'Timer has ticked
          SetIntFlags Or flgTmrAndSer, mskTmrAndSer   'Set timer 0 or hSerial to interrupt
			 Return
'
' ************************************************************
 
#3
Code part 2: Subroutines
Code:
'
' ********       S U B R O U T I N E S       ********
'
' ***** hReceive: Receive serial data via the background serial port and process
'
' Transfers received data from Scratchpad RAM into an area of main RAM starting at bRecCount
'
' Entry: hSerPtr          Points to next byte BEYOND the last received byte
'        Ptr              Points to next byte of received data IF data has been received
'        bPktLen          = 0
'  Uses: wSearchPtr       Source pointer for scratchpad data
'        wBufferedDataLen Byte count for current GPS data packet
'        wPktDataPtr      Points to Start-of-Data, after the "$GPGGA," header
'  Exit: bPktLen          Number of bytes received
'        Ptr              Points to location of the next byte to be processed
'        
hReceive:Do Until Ptr = hSerPtr
            If @Ptr = "$" Then
               wPktStartPtr = Ptr
               wBufferedDataLen = hSerPtr - Ptr And mskSerInRange  'Restrict to max value 1023
               If wBufferedDataLen > 13 Then '14 or more chars in buffer
                  GoSub Check_GPS_Header     'Compare header with "$GPGGA,"
                  If tMismatch = False Then  'Have start of a good packet, with Time data
                     wBufferedDataLen = hSerPtr - Ptr And mskSerInRange  'Restrict to max 1023
                     wPktDataPtr = Ptr       'Save location of packet start "$" pointer
                     If wBufferedDataLen >= 82 Then   'Don't log until entire GPS packet received
                                             'Bit-banged SerTxd interferes with background receive
                        bPktLen = 6          'Length of "$GPGGA,"
                        GoSub hRecCheckSum   'Confirm packet validity
                        If tGoodPacket = True Then
                           If tNoGPSDataYet = True Then
                              tNoGPSDataYet = False
                              Low oLED       'Turn off LED after first valid GPS pkt received
                              SerTxd("   Checksum Rec'd=", #bRecdCSum, ", Calc'd=", #bCalcCSum)
                              SerTxd(" First valid pkt.", CR, LF)
                           EndIf
                           Ptr = wPktDataPtr 'Restore pointer to start of packet's data
                           GoSub hReadGPSData'Receive and synchonise to GPS time
                        Else
                           SerTxd("   Checksum Rec'd=", #bRecdCSum, ", Calc'd=", #bCalcCSum)
                           SerTxd(" **Bad pkt**", CR, LF)
                        EndIf
                     Else                    'Less than required data buffered
                        Ptr = wPktStartPtr   'Reset Ptr to start of packet
                     EndIf 'wBufferedDataLen >= 82
                     Exit                    'Exit Do loop after (good or bad) full packet received
                  EndIf 'tMismatch = False
               EndIf 'wBufferedDataLen > 13
            EndIf '@Ptr = "$" Then
            Inc Ptr                          'Continue search for Start-of-packet marker ($)
         Loop
         Return
'
'
' ***** hReadGPSData: Interpret time data
'
'           Header has been found previously, marking start-of-packet
'           Checksum has been confirmed, so data should be valid
'           Packet is referenced by Ptr
'           GPS Time is in 24-hour format and is converted to 12-hour + AM/PM
'           Time reference variables are updated (overwritten) on exit
'           Change +/- cTimeOffset depending on locality (+ for east; - for west)
'
hReadGPSData:  tmpHours = @ptrInc - $30 * 10 + @ptrInc - $30 + cTimeOffset  ' + or - cTimeOffset
               tmpMinutes = @ptrInc - $30 * 10 + @ptrInc - $30
               tmpSeconds = @ptrInc - $30 * 10 + @ptrInc - $30
               If oLEDValue = On Then           'Toggle LED on every valid GPS packet
                  Low oLED
               Else
                  High oLED
               EndIf
               If tmpHours > 243 Then           'After subtracting up to 12 hours
                  tmpHours = tmpHours + 24
               EndIf
               If tmpHours > 23 Then            'After adding up to 12 hours
                  tmpHours = tmpHours - 24
               EndIf
               bHours24 = tmpHours
               Select Case tmpHours
               Case 0                           '0 is 12 (Midnight)
                  tmpHours = 12
                  bAMPM = "A"
               Case < 12                        'Morning
                  bAMPM = "A"
               Case = 12                        '12 noon
                  bAMPM = "P"
               Else                             '1PM to 11PM Afternoon/Evening
                  tmpHours = tmpHours - 12      'bHours24 - 12
                  bAMPM = "P"
               End Select
               '
               SetIntFlags Off                  'Stop interrupts while time master variables are being updated
               bHours12 = tmpHours
               bMinutes = tmpMinutes
               bSeconds = tmpSeconds
               SetIntFlags Or flgTmrAndSer, mskTmrAndSer 'Set timer 0 or hSerial to interrupt
               Return
'
' **** hRecCheckSum: Search Data Packet for EOP marker '*' and checksum value, then validate data with checksum
'
'GPS Packet example:
'    $GPGGA,235917.000,4025.6301,N,08654.7184,W,2,07,1.1,186.1,M,-33.8,M,0.8,0000*4Ecl   (cl = <cr><lf>)
'Messages have a maximum length of 82 characters, including the $ starting character and the ending <LF>
' Checksun is 'built' by XORing every character between '$' and '*'
'  Entry:   wPktStartPtr      Points to '$' character
'           bPktLen = 6       Gets Reset
'  Used:    bTemp
'           Ptr
'  Exit:    bCalcCSum
'           bRecdCSum
'           bPktLen           Packet length
'
hRecCheckSum:  Ptr = wPktStartPtr
               bCalcCSum = 0
               tGoodPacket = False
               bTemp = @PtrInc
               #IfDef Show_Packet_Diags
                  SerTxd(bTemp, @Ptr)              '$ sign + 'G'
               #EndIf
               bCalcCSum = @PtrInc
               bPktLen = 2
               Do Until @Ptr = "*" Or bPktLen > 82 'Create "Recalculated" checksum
                  #IfDef Show_Packet_Diags
                     SerTxd(@Ptr)
                  #EndIf
                  bCalcCSum = bCalcCSum Xor @PtrInc
                  Inc bPktLen
               Loop
               #IfDef Show_Packet_Diags
                  SerTxd(@Ptr)                     'Should be *'
               #EndIf
               Inc Ptr
               GoSub RecHex                        'Returns bRecdCSum, "Received" checksum
               If bCalcCSum = bRecdCSum Then       'Compare "Recalculated" with "Received" checksum
                  tGoodPacket = True
               EndIf
               #IfDef Show_Packet_Diags
                  SerTxd(CR, LF)
               #EndIf
               Return
'
' **** RecHex: Read two ASCII Hex bytes representing nibbles
'
'        First character is high nibble; Second is Low
' Entry: Ptr         Points to 1st Byte (Hex nibble - should be ASCII value "0"-"9","A"-"F")
' Used:  bNibble     
' Exit:  bRecdCSum   Binary value 0-255
'        Ptr         Points to the next Byte to be interpretted
'        tRecError   Set if bad byte value received
'
RecHex:  
         #IfDef Show_Packet_Diags
            SerTxd("[$", @Ptr)
         #EndIf
         tRecError = False
         If @Ptr >= "0" and @Ptr <= "9" Then
            bNibble = @Ptr - "0"    'Convert to 4 bits 0000 - 1001
         ElseIf @Ptr >= "A" and @Ptr <= "F" Then   
            bNibble = @Ptr - 55     'Convert to 4 bits 1010 - 1111
         Else
            bError = @ptr
            tRecError = True
         EndIf
         If tRecError = False Then
            Inc Ptr
            bRecdCSum = bNibble << 4
            #IfDef Show_Packet_Diags
               SerTxd(@Ptr, "]")
            #EndIf
            If @Ptr >= "0" and @Ptr <= "9" Then
               bNibble = @Ptr - "0"    'Convert to 4 bits 0000 - 1001
            ElseIf @Ptr >= "A" and @Ptr <= "F" Then   
               bNibble = @Ptr - 55     'Convert to 4 bits 1010 - 1111
            Else
               bError = @ptr
               tRecError = True
            EndIf
            If tRecError = False Then
               Inc Ptr
               bRecdCSum = bRecdCSum Or bNibble
            EndIf
         EndIf
         Return
'
' **** Check_GPS Header: Confirm Data with EEPROM
'
'    Entry: Ptr           Points to a "$" sign: the start of a GPS Data Packet
'     Used: bEEPROMPtr
'           bData
'           tMismatch
'
Check_GPS_Header: tMismatch = False
                  bEEPROMPtr = eGPSHeader
                  Read bEEPROMPtr, bData
                  Do
                     If @ptrInc <> bData Then
                        tMismatch = True           'Exit to resend on data mismatch
                        Exit                       'Exit 'Do' loop
                     EndIf
                     Inc bEEPROMPtr
                     Read bEEPROMPtr, bData
                  Loop Until bData = 0             'Exits on second word comparison.
                  Return
'
' ***** ShowTime: Show received wireless data packet time
'
' Entry: bHours12, bMinutes, bSeconds, bAMPM
'
ShowTime:   SerTxd ("Local time is ", #bHours12, ":")
            If bMinutes < 10 Then
               SerTxd ("0")
            EndIf
            SerTxd (#bMinutes, ":")
            If bSeconds < 10 Then
               SerTxd ("0")
            EndIf
            SerTxd (#bSeconds)
            If bAMPM > " " Then
               SerTxd (" ", bAMPM, "M")
            EndIf
            SerTxd (CR, LF)
            '
            Return
 
Top