Big GPS Clock

GPS_Clock

What's not to like about the idea? A GPS module is the perfect companion to the humble clock and ensures the time is always accurate and never needs adjusting (unless you live in an area that has daylight savings).

Better still, the GPS module does not require a "lock" to multiple satellites - just one or more. "I want to build one!" I hear you say - well read on...

 

GPS + Time

Strings are continuously sent from a GPS device and are formatted in easy-to-use NMEA standard. Different NMEA sentences have different fields of information. For this project, I don't care where the clock is positioned on the earth, nor do I have any concern for its altitude or the date. I just want the time information.

Before you throw this project in the "too hard basket", it might be handy to know that the developer of Swordfish (David Barker) created a module which automatically retrieves NMEA sentences (via the use of interrupts). The module can be found in the User Module Pack and is called "NMEA.bas". Here's an example of filtering all unwanted data, retrieving only time information:

GPS NMEA Time Extraction (Swordfish Basic)
Dim Time as String(12)
 
Sub GetGPSTime()
If NMEA.GetItem(NMEAItem) And NMEAItem.Valid Then
NMEA.GetField(NMEAItem,0,Field)
If Field = "$GPRMC" Then
NMEA.GetField(NMEAItem, 1, Field)
Time = Field
EndIf
EndIf
End Sub

 

Update - NMEA2.bas

I had LOTS of issues with the NMEA.bas library stomping all over shared RAM variables, and decided to create my own module rather than debug Davids. It's versatile, and should work with any GPS module. It is called NMEA2.bas and full use/download links can be found in this article.

 

Choose a GPS Module

It really doesn't matter which GPS module you use. I am using a LS20031 GPS Module as I have one handy. For ease of use, selecting a 5V tolerant device would probably be the way to go. Mine is 3.3 volts, so I had to work around that feature.

Another aspect to keep in mind is that GPS signals are quite hard to receive indoors. Some modules simply do not offer the gain required. The LS20031 was able to consistently "lock-on" anywhere in my house, while my TomTom was only able to near windows.

With that in mind, most GPS modules have a built in RTC and will continue to output RMC sentences (which contain time information) regardless of satellite reception. If that's your case, then you will need to move the clock near a window every few months to sync the time.

 

BIG Displays

Next on the list of things with a clock is a legible display. I've made a couple of clock iterations  over time, and found the price of large segment displays to be a little hefty. And then I stumbled onto the 4" Segment Displays from Sure Electronics. $16 for a board with two 4" displays and all the necessary driving components was an absolute steal.

4_INCH_Display

The above image does not give them the justice... They are physically 4 inches from bottom to top of the segment. The whole board is a little over 5.5 inches in height.

Myself and others found the display hard to read from a distance due to the white segments that were not lit (even when powered by 12 volts). I resolved this by masking the displays with a white vinyl (sticker) - that way only the lit segments would be visible and there's much less information to interpret. Here's a before photo of the segments:

GPS_Clock_Segments

And an after photo with the vinyl (sticker) applied:

GPS_Clock_Segments_Covered

Interfacing with the displays was a little interesting. Datasheet suggests how-to send data to the displays, though I had several issues with daisy chaining them. I tried a few different variations, and ended up settling with controlling each set of two separately. When (and if) I refine the daisy chaining, I will update this article! (edit: I have had some great guidance from Jerry in the Digital DIY forums and will make some changes in the near future)

 

Schematic

Keeping in mind I am using a 3.3V GPS Module, so it only makes sense to drive the PIC at 3.3V as well. I breadboarded the project to ensure the 4" Segment Displays were 3.3V tolerant - they were.

GPS_Clock_Schematic

 

Source Code

The program is heavily commented and should be easy to follow. Feel free to post comments with any questions.

GPS Clock (Swordfish Basic)
{
*****************************************************************************
*  Name    : GPS Clock.bas   (project number 0049)                          *
*  Author  : Graham Mitchell                                                *
*  Notice  : Copyright (c) 2010 Graham Mitchell 2010                        *
*          : All Rights Reserved                                            *
*  Date    : 26/07/2010                                                     *
*          :                                                                *
*  Version : 1.0                                                            *
*  Notes   : A GPS clock... There's too many benefits to not make one. The  *
*          : time is ALWAYS accurate, and there's no need to ever make      *
*          : adjustments.                                                   *
*          :                                                                *
*          : The initial release interfaces with a LS20031 GPS Module,      * 
*          : although, any GPS module could be used.                        *
*          :                                                                *
*          : NMEA sentences are monitored until a valid RMC sentence is     *
*          : received. Time information is extracted and converted from a   *
*          : string to decimal numbers.                                     *
*          :                                                                *
*          : Time information is then advanced depending on the users time  *
*          : settings. Advancements work in 15 minute increments by         *
*          : holding the "AdjTime" button. User settings are saved to the   *
*          : EEPROM in case of power failure.                               *
*          :                                                                *
*  Version : 1.1                                                            *
*          : LOTS of changes.. NMEA.bas was stomping all over my variables  *
*          : so I have written a custom interrupt to grab time fields.      *
*          :                                                                *
*          : With the new found flexibility, time adjustments are handled   *
*          : by single button presses + button debounces controlled by TMR2.*
*          :                                                                *
*          : Probably the best change has been the addition of a custom     *
*          : library for the DE-DP004 Sure Electronics display boards. More *
*          : information regarding the library can be found in the module   *
*          : comments                                                       *
*          :                                                                *
*          : Time advancements have been changed to 1 hour for ease of      *
*          : configuration - you can change this to what ever suite you     *
*          :                                                                *
*          : EEPROM is preloaded with EST time zone adjustment (+10 hours)  *
*          :                                                                *
*  Version : 1.2                                                            *
*          : Fixed a bug which determines end of month roll-over events     *
*          : during time advancement.                                       *
*          :                                                                *
*          : Added some additional commenting regarding TMR2.               *
*          :                                                                *
*          : Changed the time/date display ratio to 5.25 seconds for time,  *
*          : and 2 seconds for date.                                        *
*          :                                                                *
*****************************************************************************
}
 
// define device, clock and disable MCLRE
Device = 18F2520
Clock = 32
Config MCLRE = Off
 
#option DE_Data = PORTC.3
#option DE_CLK = PORTC.5
#option DE_DIMM = PORTC.2
 
#option NMEA_BUFFER_SIZE = 63
 
Include "InternalOscillator.bas"    // this module configures the internal oscillator to operate at 32Mhz from the get-go
Include "USART.bas"                 // the USART module is required to configure the baud rate
Include "NMEA2.bas"
Include "EEPROM.bas"                // EEPROM module for saving settings
Include "UTILS.bas"                 // the utils module is used for the function "Utils.Digit"
Include "DE_DP004.bas"              // user module for the Sure Electronics DE-DP004 displays
 
// preload EEPROM with EST time adjustment
EEPROM = ($AA,$00,$00,$0A)
 
Dim Time As DE_DP004.TTime,
    strTime As String(12),
    strDate As String(7)
    LastSecond As Byte,
    DecimalOn As Boolean
 
// TMR2 variables
Dim TMR2IE As PIE1.1, TMR2IF As PIR1.1, TMR2ON As T2CON.2, TMR2IP As IPR1.1, ButtonDebounce As Word, Debounce As Byte, mS As Word
Const mS_Inc = 8, DebounceDelay = 50
Const DaysInMonth(12) As Byte = (31,28,31,30,31,30,31,31,30,31,30,31)
 
// time adjust structure           
Structure TAdjust
    Hours As Byte
    Minutes As Word
End Structure
Public Dim Adj As TAdjust
 
// date structure           
Structure TDate
    Day As Byte
    Month As Word
End Structure
Public Dim Date As TDate
 
// define I/Os
Dim AdjTime As PORTB.0
 
 
// save the user adjustment settings to EEPROM
Sub SaveEEPROMSettings()    
    EE.Write(0,$AA,Adj.Minutes,Adj.Hours)            
End Sub
 
// load user adjustment settings from EEPROM. Should no information be found, then it loads zero for both minutes and hours.
Sub LoadEEPROMSettings()
    Dim tmpByte As Byte
    EE.Read(0,tmpByte)
    If tmpByte <> $AA Then
        EE.Write(0,$AA,$00,$00,$00)
    EndIf
 
    EE.Read(1,Adj.Minutes,Adj.Hours)            
End Sub
 
// convert a single ascii character to decimal
Function CharToDec(ByVal pChar As Byte) As Byte
    Result = pChar - 48
End Function
 
// check if the button "AdjTime" is pressed and increment adjustment registers as necessary
Sub CheckTimeAdjust()
    If Adj.Hours = 10 Then
        Adj.Hours = 11
    Else
        Adj.Hours = 10
    EndIf
    SaveEEPROMSettings()    
End Sub
 
// capture the last day of month roll-over, and correct
Sub ValidateTheDate()
    Dim LastDay As Byte
 
    LastDay = DaysInMonth(Date.Month - 1)
 
    If Date.Day > LastDay Then
        Date.Day = Date.Day - LastDay
        Date.Month = Date.Month + 1
    EndIf
End Sub
 
// advance the time based on the adjustment registers
Sub AdjustTime()
    Dim ValidateDate As Boolean
 
    ValidateDate = false
    Time.Minutes = Time.Minutes + Adj.Minutes
    Time.Hours = Time.Hours + Adj.Hours
 
    While Time.Minutes > 59
        Time.Minutes = Time.Minutes - 60
        Time.Hours = Time.Hours + 1
    Wend
 
    While Time.Hours > 23
        Time.Hours = Time.Hours - 24
        Date.Day = Date.Day + 1
        ValidateDate = True
    Wend
 
    If ValidateDate Then
        ValidateTheDate()
    EndIf
End Sub
 
// convert the date from string to decimal
Sub ConvertDate()
    Date.Day = CharToDec(strDate(1)) + (CharToDec(strDate(0))*10)
    Date.Month = CharToDec(strDate(3)) + (CharToDec(strDate(2))*10)    
End Sub
 
// update the segment displays to show HHMM
Sub DisplayTime()
    If Time.Seconds <> LastSecond Then
        LastSecond = Time.Seconds        
        DecimalOn = Not DecimalOn
    EndIf
    DE_DP004.Write(Time, DecimalOn)
End Sub
 
// update the segment displays to show DDMM
Sub DisplayDate()
    DE_DP004.Write(Word((Date.Day * 100) + Date.Month),false,true,false,false)
End Sub
 
// convert the passed string to decimal time
Sub StringToTime(ByRef pString As String) 
    Time.Seconds = (CharToDec(pString(4))*10) + CharToDec(pString(5))
    Time.Minutes = (CharToDec(pString(2))*10) + CharToDec(pString(3))
    Time.Hours = (CharToDec(pString(0))*10) + CharToDec(pString(1))   
End Sub
 
// checks for new NMEA data
Function CheckForNMEA() As Boolean
    Result = NMEA.NewItem 
End Function
 
// extracts the time field from NMEA sentence
Sub GetTimeDate()
    NMEA2.GetField(1,strTime)
    NMEA2.GetField(9,strDate)
 
    NMEA.NewItem = False
 
    ConvertDate()                             // convert the date from string to decimal
    StringToTime(strTime)                      // convert the time from string to decimal
End Sub
 
// TMR2 interrupt - handles adjust time button debounces (low priority interrupt)
Interrupt TMR2_Interrupt(1)
    If TMR2IF = 1 Then                                        // check if the interrupt was from TMR2
        TMR2IF = 0                                            // clear TMR2 interrupt flag
        mS = mS + mS_Inc
        If mS >= 6250 Then
            mS = 0
        EndIf
        If Debounce = 1 Then                                  // check to see if debounce is enabled            
            If ButtonDebounce > DebounceDelay Then            // check if debounce time has elapsed
                If AdjTime = 1 Then                           // check if button has been depressed
                    Debounce = 0                              // clear the debounce flag                    
                EndIf
            Else
                ButtonDebounce = ButtonDebounce + mS_Inc      // increment button debounce variable            
            EndIf
        EndIf
    EndIf
End Interrupt
 
// initialise TMR2 for 8-msec interrupts (32-MHz clock)
Private Sub Initialise_TMR2()
    TMR2ON = 0                 // disable TMR2
    TMR2IE = 0                 // turn off TMR2 interrupts   
    PR2 = 249                  // TMR2 Period register PR2
    T2CON = %01111011          // T2CON 0:1 = Prescale
                               //        00 = Prescaler is 1:1
                               //        01 = Prescaler is 1:4
                               //        1x = Prescaler is 1:16                                 
                               //      3:6 = Postscale              
                               //     0000 = 1:1 postscale
                               //     0001 = 1:2 postscale
                               //     0010 = 1:3 postscale...
                               //     1111 = 1:16 postscale
    Debounce = 0               // clear debounce flag
    TMR2 = 0                   // reset TMR2 Value
    mS = 0                     // reset mS register
    TMR2IP = 0                 // set TMR interrupt priority LOW
    TMR2IE = 1                 // enable TMR2 interrupts
    TMR2ON = 1                 // enable TMR2 to increment  
    Enable(TMR2_Interrupt)     // enable handler
End Sub
 
// initialise the program/PIC
Sub InitialisePIC()
    // init variables
    Time.Hours = 0
    Time.Minutes = 0
    Time.Seconds = 0
    LastSecond = 255
    DecimalOn = True
    // enable weak pullups
    INTCON2.7 = 0
    Input(AdjTime)                
    // load saved settings (if they exist)
    LoadEEPROMSettings()    
    // initialise NMEA module    
    USART.SetBaudrate(br38400)
    NMEA2.Initialise    
    // initialise TMR2
    Initialise_TMR2()    
End Sub
 
 
// main program start...
InitialisePIC()
 
// main program loop
While True
    If CheckForNMEA Then                          // check if new data has been received
        GetTimeDate()                             // extract the time field from RMC sentence                
        AdjustTime()                              // adjust new time with user settings
        If mS < 4250 Then            
            DisplayTime()                         // display the adjusted time
        Else
            DisplayDate()                         // display the adjusted date
        EndIf
    EndIf
    If AdjTime = 0 And Debounce = 0 Then          // check if adjust time is pressed (and debounce is disabled)
        CheckTimeAdjust()                         // handle time adjustments
        ButtonDebounce = 0                        // reset the button debounce variable
        Debounce = 1                              // enable the debounce handle
    EndIf
Wend

 

Put It All Together

The project is really quite simple between the schematic and the source code. I made a small wooden box to house the equipment. Here are some completed photo's of the project:

GPS_Clock_Main_Board

The clock now lives at work where the old clock used to be (it was horrible for keeping the time!).GPS_Clock_Workshop

 


Posted: 4 years 2 months ago by hop #2385
hop's Avatar
I am really interested in these! I tried quite awhile ago to build a big 6 digit display using a MAX controller and failed miserably when I realized all the wiring I needed to do to connect the two displays. There were certainly easier ways to go about it, but it didn't work. Even after few hours effort and two full kits of static sized jumper wires.

As you know Graham, I'm moving forward (albeit slowly) with my Embedded Jeep project and one of the features is a custom maded console that shows external and internal temps, barometric pressure, GPS location, and direction. All these features are available on newer vehicles and also was an option for my Jeep when I bought it in '98, but at a substantial cost not worthy of the benefit. Since I found that I can get the mod case from a junk yard and that I have hard points on my Jeep for it, I'm exploring populating the console with my own creation. This device might be a candidate for that project.

Not sure your daisy chain issues and the cause of the problems. I'm sure you checked the data on a scope to see how the corruption might be affected. Since I am looking at this hardware, I'll get two and test it as well and let you know what I find out.

I noticed in the spec notes that it says it is not possible to connect them in series. On that note, I'm wondering if simple logic channeling the data and clock to a selected pair might do the trick. I'll explore that too.

Hop
Posted: 4 years 2 months ago by hop #2386
hop's Avatar
Another note... I wonder if the PWM is for both digits in the display OR for each segment. The reason I wonder about that is that it would be very cool to control the PWM for each segment to give that "24" effect for an unnecessary bling factor.


If not, I can use that MAX controller I mentioned in another thread (about using PWM to control my boat lighting) to control each digit of the display, and see if multiplexing two digits per controller will affect brightness significantly.

Hop
Posted: 4 years 2 months ago by jmessina #2392
jmessina's Avatar
The DIM signal is connected to both displays, so you can't pwm just one.

I think the problem you're having with daisy-chaining the displays is probably due to the way they've connected the HC595 shift register controls. The HC595 has two sets of registers: the shift register (controlled by SCLK) and an output latch (controlled by RCLK). They've connected the two together which is perfectly valid, but it means that the output register is always 1 clock behind, or to put it another way, after you clock out the 16 bits of data into the two shift registers on the board, you need to add a 17th clock pulse to latch the data into the output latch. You can see this in the sample code in the datasheet...after the two 8-bit loops there's an additional clock.

If you're daisy-chaining modules, this 17th clock pulse is going to clock the wrong data into the next module. Re-write the driver to send out all of the 8-bit digit data for as many displays as you have, and then add a single extra clock at the end.

Also, the modules are going to be fairly slow...HC logic isn't the fastest of choices. Since they've added additional buffering to the CLK line, make sure that you have at least 100ns data setup and hold time between the serial data output pin and the clock pin transitions, and pay attention to the clock frequency called out in the spec sheet, esp as you daisy-chain modules.

Jerry
Posted: 4 years 1 month ago by Graham Mitchell #2396
Graham Mitchell's Avatar
Thanks for the info regarding the shift registers Jerry - I will update the program and see how things go. I've left the segments at work today (needed to make some measurements). The code as it stands now (I'll try your changes when I have the segments in hand):
{
*****************************************************************************
*  Name    : GPS Clock.bas   (project number 0049)                          *
*  Author  : Graham Mitchell                                                *
*  Notice  : Copyright (c) 2010 Graham Mitchell 2010                        *
*          : All Rights Reserved                                            *
*  Date    : 19/07/2010                                                     *
*          :                                                                *
*  Version : 1.0                                                            *
*  Notes   : A GPS clock... There's too many benefits to not make one. The  *
*          : time is ALWAYS accurate, and there's no need to ever make      *
*          : adjustments.                                                   *
*          :                                                                *
*          : The initial release interfaces with a LS20031 GPS Module,      * 
*          : although, any GPS module could be used.                        *
*          :                                                                *
*          : NMEA sentences are monitored until a valid RMC sentence is     *
*          : received. Time information is extracted and converted from a   *
*          : string to decimal numbers.                                     *
*          :                                                                *
*          : Time information is then advanced depending on the users time  *
*          : settings. Advancements work in 15 minute increments by         *
*          : holding the "AdjTime" button. User settings are saved to the   *
*          : EEPROM in case of power failure.                               *
*          :                                                                *
*****************************************************************************
}

// define device, clock and disable MCLRE
Device = 18F2520
Clock = 32
Config MCLRE = Off

// this module configures the internal oscillator to operate at 32Mhz from the get-go
Include "InternalOscillator.bas"
// the USART module is required to configure the baud rate
Include "USART.bas"
// NMEA module handles interrupt driven NMEA sentence retrieval
Include "NMEA.bas"
// EEPROM module for saving settings
Include "EEPROM.bas"
// the utils module is used for the function "Utils.Digit"
Include "UTILS.bas"


// program variables
Dim NMEAItem As TNMEA
Dim Field As String
Dim Skip As Byte

Structure TTime
    Hours As Byte
    Minutes As Byte
    Seconds As Byte
End Structure
Public Dim Time As TTime

Structure TAdjust
    Hours As Byte
    Minutes As Word
End Structure
Public Dim Adj As TAdjust

// pinouts for segments
Dim Data As PORTC.3, CLK As PORTC.5, DIMM As PORTC.2
Dim Data2 As PORTA.1, CLK2 As PORTA.2, DIMM2 As PORTA.0
Dim AdjTime As PORTB.0

// save the user adjustment settings to EEPROM
Sub SaveEEPROMSettings()    
    EE.Write(0,$AA,Adj.Minutes,Adj.Hours)            
End Sub

// load user adjustment settings from EEPROM. Should no information be found, then it loads zero for both minutes and hours.
Sub LoadEEPROMSettings()
    Dim tmpByte As Byte
    EE.Read(0,tmpByte)
    If tmpByte <> $AA Then
        EE.Write(0,$AA,$00,$00)
    EndIf
    
    EE.Read(1,Adj.Minutes,Adj.Hours)            
End Sub

// convert a single ascii character to decimal
Function CharToDec(ByVal pChar As Byte) As Byte
    Result = pChar - 48
End Function

// convert the passed string to decimal time
Sub StringToTime(ByRef pString As String) 
    Time.Seconds = (CharToDec(pString(4))*10) + CharToDec(pString(5))
    Time.Minutes = (CharToDec(pString(2))*10) + CharToDec(pString(3))
    Time.Hours = (CharToDec(pString(0))*10) + CharToDec(pString(1))   
End Sub

// function which returns true if a valid NMEA RMC sentence is in the buffer
Function GetGPSTime() As Byte
    Result = 0
    If NMEA.GetItem(NMEAItem) And NMEAItem.Valid Then
        NMEA.GetField(NMEAItem,0,Field)
        If Field = "$GPRMC" Then
            NMEA.GetField(NMEAItem, 1, Field)
            StringToTime(Field)
            Result = 1       
        EndIf
    EndIf
End Function

// check if the button "AdjTime" is pressed and increment adjustment registers as necessary
Sub CheckTimeAdjust()  
    If AdjTime = 0 Then
        Adj.Minutes = Adj.Minutes + 15
        If Adj.Minutes > 59 Then
            Adj.Minutes = Adj.Minutes - 60
            Adj.Hours = Adj.Hours + 1
            If Adj.Hours = 24 Then
                Adj.Hours = 0
            EndIf
        EndIf
        SaveEEPROMSettings()
    EndIf
    
End Sub

// advance the time based on the adjustment registers
Sub AdjustTime()
    Time.Minutes = Time.Minutes + Adj.Minutes
    Time.Hours = Time.Hours + Adj.Hours
    
    While Time.Minutes > 59
        Time.Minutes = Time.Minutes - 60
        Time.Hours = Time.Hours + 1
    Wend

    While Time.Hours > 23
        Time.Hours = Time.Hours - 24
    Wend
End Sub

// convert a decimal number to segment data
Function DecToSeg(ByVal pValue As Byte) As Byte
    Result = 0
    
    Select pValue
        Case 0
            Result = $FC
        Case 1
            Result = $60
        Case 2
            Result = $DA
        Case 3
            Result = $F2
        Case 4
            Result = $66
        Case 5
            Result = $B6
        Case 6
            Result = $BE
        Case 7
            Result = $E0
        Case 8
            Result = $FE
        Case 9
            Result = $F6
    End Select                
End Function

// send segment data to the displays
Sub SendBytes(ByVal pSeg0,pSeg1,pSeg2,pSeg3 As Byte)
    Dim i As Byte
    
    For i = 0 To 7
        Data = pSeg0 And $01
        CLK = 0
        CLK = 1
        pSeg0 = pSeg0 >> 1
    Next   
    For i = 0 To 7
        Data = pSeg1 And $01
        CLK = 0
        CLK = 1
        pSeg1 = pSeg1 >> 1
    Next   
    CLK = 0
    CLK = 1

    For i = 0 To 7
        Data2 = pSeg2 And $01
        CLK2 = 0
        CLK2 = 1
        pSeg2 = pSeg2 >> 1
    Next   
    For i = 0 To 7
        Data2 = pSeg3 And $01
        CLK2 = 0
        CLK2 = 1
        pSeg3 = pSeg3 >> 1
    Next   
    CLK2 = 0
    CLK2 = 1       
End Sub

// update the segment displays with current time
Sub DisplayTime()
    DIMM = 1
    DIMM2 = 1
    
    If Time.Seconds Mod 2 = 0 Then
        SendBytes(DecToSeg(Digit(Time.Minutes,1)),DecToSeg(Digit(Time.Minutes,2)),DecToSeg(Digit(Time.Hours,1)),DecToSeg(Digit(Time.Hours,2)))
    Else
        SendBytes((DecToSeg(Digit(Time.Minutes,1)) Or $01),DecToSeg(Digit(Time.Minutes,2)),DecToSeg(Digit(Time.Hours,1)),DecToSeg(Digit(Time.Hours,2)))
    EndIf
       
    DIMM = 0
    DIMM2 = 0
End Sub


// initialise the PIC and variables
Sub InitialisePIC()
    // configure I/Os
    SetAllDigital
    Low(Data)
    High(CLK)
    High(DIMM)
    
    Low(Data2)
    High(CLK2)
    High(DIMM2)
    
    // enable weak pullups
    INTCON2.7 = 0
    Input(AdjTime)        
    
    // configure USART module
    USART.SetBaudrate(br38400)
    
    // load saved settings (if they exist)
    LoadEEPROMSettings()

    // initialise variables
    Skip = 0
End Sub


// main program start...
InitialisePIC()

// main program loop
While True
    // GPS Outputs at 5Hz, sample every 3rd update (this creates a delay between time adjustments)
    If GetGPSTime() = 1 Then
        Inc(Skip)
        If Skip = 2 Then
            Skip = 0
            CheckTimeAdjust()
            AdjustTime()
            DisplayTime()
        EndIf
    EndIf  
Wend
I was having issues with corrupt data with the use of delays, and a quick work-around was to simply grab every 3rd NMEA sentence as it arrived. I can invoke the behaviour by changing the main program loop to the following:
// main program loop
While True
    If GetGPSTime() = 1 Then
        CheckTimeAdjust()
        AdjustTime()
        DisplayTime()
        DelaymS(500)
    EndIf  
Wend
The variables Adj.Minutes and Adj.Hours store the time advancement for local time correction. It would appear as if the program is corrupting the variables as the time changes erratically on the displays. I was able to isolate the RAM corruption to those two variables by changing the "AdjustTime" sub routine like so:
// advance the time based on the adjustment registers
Sub AdjustTime()
    // preload the corrupt RAM registers with zero
    Adj.Minutes = 0
    Adj.Hours = 0

    Time.Minutes = Time.Minutes + Adj.Minutes
    Time.Hours = Time.Hours + Adj.Hours
    
    While Time.Minutes > 59
        Time.Minutes = Time.Minutes - 60
        Time.Hours = Time.Hours + 1
    Wend

    While Time.Hours > 23
        Time.Hours = Time.Hours - 24
    Wend
End Sub
The above change preloads both variables before any time adjustments. The result is UTC time displayed without any odd behaviour.

From what I can gather, the user module "NMEA.bas" (which can be downloaded from the User Module Pack) has the appropriate context saving in place. Does anything stand out to you Jerry?
Posted: 4 years 1 month ago by jmessina #2397
jmessina's Avatar
It sounds like there's some sort of buffer issue going on...if you wait the 5 seconds then you've gotten ~25 NMEA messages, which is quite a few characters. I took a quick glance it NMEA.bas and saw something I don't like to see, and that's trying to parse the message string inside the interrupt handler instead of just buffering the characters and processing them outside of the interrupt context. It does seem to have some optimizations where it's using pic registers to hold and pass data, and that's always a potential source of problems. Let me take a closer look at how it handles buffer overflows, uart errors and such...
Posted: 4 years 1 month ago by jmessina #2399
jmessina's Avatar
nmea.bas has some serious buffer handling issues. If you ever start dropping chars from the gps (which you will if the nmea.FBuffer array ever fills up), there's a very good chance that the nmea.getitem() and nmea.getfield() routines will start marching all over your memory. There's next to no error handling in the entire module, and with the exception of a check to see if FBuffer has gotten too many characters, there's no checks on the length of strings that it's copying, or whether the data makes any sense or not.

IF you never drop a character from the gps, never get a uart error, never get a mal-formed nmea string, and manage to process every single character that comes in, your good to go. If you don't manage to get the characters out of the buffer before FOverflow ever gets set, then you're probably hosed. You're lucky if all that happened was it corrupted your AdjTime variables.

As a simple example, trace through the logic and see what happens if you get a string of 81 "$" characters followed by a "whitespace" character like a CR. (hint: take a look at the declaration for TNMEA.Line()). While this is a very unlikely scenario, if you start dropping characters or get unexpected data there are a number of these "gotcha" situations.

The comment in the source code for "1.1 - Added overflow flag to prevent overwriting of the ring buffer" is true... it won't over-write the ring buffer. But the rest of the comment about the "interrupt will re-sync with any NMEA sentence" is wishful thinking. There are a number of loops that will run until a null character is found without regard to how much data it's processing. About the only thing that stops the module from blowing up all the time is the checksum validation and the NMEAItem.Valid flag.

IMHO, the module needs to be trashed.
Posted: 4 years 1 month ago by MMcLaren #2400
MMcLaren's Avatar
Jerry,

That's pretty harsh but you obviously studied the module much more then I did.

I was going to suggest Graham not use that module and just qualify each $GPRMC sentence manually. At 4800 baud he has a whopping 2-msecs between incoming characters.

I did it something like this on a 10F200 project (bit banged serial and LCD). When the parser is off the routine throws away characters until the next '$' character is received.

Regards, Mike
;
;  const rom char hdr[] = { "GPRMC" };    //
;  const rom char seg[] = { 0b00111111,   // "0"   -|-|F|E|D|C|B|A
;                           0b00000110,   // "1"   -|-|-|-|-|C|B|-
;                           0b01011011,   // "2"   -|G|-|E|D|-|B|A
;                           0b01001111,   // "3"   -|G|-|-|D|C|B|A
;                           0b01100110,   // "4"   -|G|F|-|-|C|B|-
;                           0b01101101,   // "5"   -|G|F|-|D|C|-|A
;                           0b01111101,   // "6"   -|G|F|E|D|C|-|A
;                           0b00000111,   // "7"   -|-|-|-|-|C|B|A
;                           0b01111111,   // "8"   -|G|F|E|D|C|B|A
;                           0b01101111 }; // "9"   -|G|F|-|D|C|B|A
;
;  unsigned char field = 3;               // parser off initially
;  unsigned char ndx = 0;                 // field index
;  unsigned char time[10];                // time string
;
;  #define parser (field < 3)             // parser on flag
;
;  void main()
;  { init();                              // init hardware
;    while(1)                             // loop
;    { data = Get232();                   // get gps character
;      if(data == '$')                    // if new 'sentence'
;      { field = 0; ndx = 0;              // reset field and index
;      }                                  //
;      else                               // not new sentence
;      { if(parser)                       // if parser 'on'
;        { if(data == ',')                // if new field
;          { field++; ndx = 0;            // bump field, reset index
;          }                              //
;          else                           // not new field
;          { if(field == 0)               // if "header" field
;              if(data!=hdr[ndx])         // if not "GPRMC"
;                field = 3;               // turn parser off
;            if(field == 1)               // if "time" field
;              time[ndx] = data;          // add char to time string
;            if(field == 2)               // if "valid" field
;              if(data == "V")            // if valid data
;                display();               // update LED display
;            ndx++;                       // bump field index
;          }                              //
;        }                                //
;      }                                  //
;    }                                    //
;  }                                      //
;
Posted: 4 years 1 month ago by jmessina #2401
jmessina's Avatar
Yeh, Mike, I guess that was a bit harsh.

While the example I gave was very contrived, the problem is that the module has a number of places where it will blow up if things go awry. Defensive Programming 101 teaches "what can go wrong will go wrong", especially when you're dealing with input from the outside world, and this is a good example. Graham wanted to do something seemingly simple, and the module even has a comment that leads you to believe "Hey, no problem. I already took care of that for you".

How much trouble is it to do some sanity checks on things like the length of strings BEFORE you copy them, or make sure that you're not putting 200 characters into an 80 character buffer, or make sure that you didn't get 100 checksum characters, etc. On second thought, it IS a lot of trouble, but that's what it's all about when writing a good program. NMEA.bas doesn't fall into that category.

It's a good thing the GPS clock isn't launching a missile...

Jerry
Posted: 4 years 1 month ago by MMcLaren #2402
MMcLaren's Avatar
Graham,

I forgot to say that those displays look extremely nice. Are you just using two of them?

Regards, Mike
Posted: 4 years 1 month ago by Graham Mitchell #2405
Graham Mitchell's Avatar
I finished the clock at work today during a "break" - though I forgot to bring it home.. which means it's still running the original code (for now!)

Thanks for your guidance Jerry and Mike, I really appreciate it. I had a feeling that my variables were being "stomped-on" by the NMEA.bas library, and your explanation makes it clear Jerry.
the module even has a comment that leads you to believe "Hey, no problem. I already took care of that for you".
It seems I have run into a number of SF quirks the last few weeks - I had good confidence in the module given it was written by David Barker himself. There's likely a few revisions which haven't been published since (either by himself, or others) - hopefully they are shared someday.

I've got some code ready to test which manually decodes the NMEA sentences (as suggested by Mike) and update the daisy chained displays (which are controlled independently atm). I took some snaps at work today of the completed enclosure]new.digitaldiy.io/images/GPS_Clock.png[/img]

Forum Activity

Member Access