Sunday, September 14, 2025

Arduino GPS Clock

This project started on a whim -- years ago I'd done some work with GPS receivers and decoding their NMEA-0183 serial data stream, and recently I thought it might be fun to make a very accurate clock using the "Time" field contained within the NMEA-0183 message stream.

The GPS receivers I've used in the past sent a packet of NEMA-0183 messages from their serial port once a second.  These messages contain information such as data and time, longitude and latitude of the receiver, and the satellites that it is receiving.  

The GPS time field contains a very accurate representation of time, formatted as HHMMSS.xxx, where HH is hours, MM is minutes, SS is seconds, and xxx represents fractions of a second.  Hours is referenced to GMT.

I only wanted to display hours and minutes -- displaying ever-changing seconds, too, would make the display too "busy", in my opinion.  So although I receive a new time from the GPS satellite network every second, the display only changes on a minute-by-minute basis.

On the front panel, in addition the four digit time display, I've mounted a photoresistor for measuring the ambient light level at the clock's front panel.  This measurement allows the display brightness to change with a room's ambient light level, i.e. minimum brightness for a dark room to maximum brightness for a very bright room.  There are four brightness steps.

I've also included a software menu system that lets me, via the front panel's four digit numerical display and two switches on the receivers back panel, select and change the following clock parameters:

  • Standard-Time offset, in hours, from UTC (e.g. set to -8 hours in California).
  • Select 12 or 24 hour format.
  • Select the Display Brightness Level (1 of 4 steps).
  • Enable Display Brightness to change with ambient light level (or remain fixed).
  • Enable the display's colon (":") between the Hour and Minute fields to "blink" (on for a second, off for a second), or remain ON constantly.
  • View the number of satellites the GPS receiver is tracking (for debugging).
  • View the analog value representing the ambient light level (for debugging).

In addition, there is a third 3-position toggle switch on the back panel that lets me select between Standard Time, Daylight Savings Time, or GMT.  Although these could have been menu items, I preferred the convenience of using a switch to select between them.

The clock is powered by +5 VDC via the Arduino Nano's mini-USB port.


Project Description:

Hardware:

GPS satellite data is received via a u-blox GY-NEO6M Rev2 GPS module using a NEO6M GPS receiver (purchased from Amazon -- interestingly, the seller disappeared several weeks later!).

This module sends a serial data-stream once a second to an Arduino Nano processor, which decodes the data-stream, strips out the time field, and displays it on a 4-digit seven-segment TM1637 numerical display.

Although the Nano receives serial data from the NEO6M receiver, the Nano does not transmit data back to the NEO6M (there really is no need to do this during normal operation of the clock).  

However, for changing the NEO6M receiver's parameters, such as its baud rate, I've included a 5-pin header onto which a CH340 USB adapter can be temporarily mounted (for this application, I use a SparkFun Serial Basic Breakout CH340G board):


The Nano's and the GPS receiver's Serial Ports operate at different logic levels -- the Nano's is 5V logic while the Receiver's is 3.3V logic.  Given the voltage drop introduced by the Nano's 5V regulator, etc., the signaling-voltage margins can be on the edge of acceptability. I did not feel comfortable with the narrowness of these margins, and so I decided it better to include simple 3-component bi-directional level shifters between the Nano's 5V I/O and the GPS Receiver's 3.3V I/O.

Such level shifters consist of two resistors and a single MOSFET transistor (e.g. BSS138) per channel.  They can be purchased, pre-assembled on PCBs, from Amazon:

There is a photoresistor, whose resistance changes with the level of ambient light, mounted on the front panel.  The Nano reads an analog value representing a voltage generated with this photo resistor, and, using this, it can adjust the display's brightness accordingly.

I wasn't sure what value of resistance for normal ambient light I'd need, so I purchased a selection of 5 different types of photoresistors to allow for experimentation:


I chose to use the "5506" (i.e. GL5506) photoresistor because its resistance, of the five choices, seemed the least likely to be loaded-down by the inherent resistance of the Nano's Analog port.

The display is a TM1637 Module, which uses a 2-wire Clock/Data interface (there is an Arduino library of TM1637 functions).  Unsure of what color to use, I purchased a pack of five modules, each with a different color:


In summary -- a very basic design consisting of three HW modules mounted on a thru-hole board (Nano, Level-Shifter, and GPS Receiver), and a separate Display module.

I brought the antenna connection out to the clock's back panel (via a uFL-male to SMA-female cable), rather than mounting a small antenna module within the clock's case the inside of the this case (scavenged from a product) had been sprayed with a conductive paint (I'm sure for EMC reasons), and I doubted an antenna mounted inside the resulting Faraday cage would have worked very well.

Below is the schematic of the clock.

Notes:

1.  There are two jumpers on the board, JP1 and JP2.  For normal operation, these should be installed on header J1 per the schematic.

  • JP1 connects the NEO6M's serial output pin to the Nano's serial input.  
  • JP2 connects the board's +5VDC to the NEO6M receiver
2.  To test a different receiver module with the board, remove Jumper's JP1 and JP2, then connect the new Receiver's module to the board as follows:
  • New Receiver's GND to J1.1.
  • New Receiver's TXD (serial data out, 3.3V level) to J1.3.
  • New Receiver's VCC (+5VDC) to J1.5.
With these connections made, the new GPS receiver should communicate with the Nano (be sure to ensure that the Nano's and the Receiver's baud rates are identical).


Software:

There are two timing loops in the software.  

The first loop is a 20 msec loop, and its primary task is switch debouncing and performing the resulting required actions.  

Note that if a clock parameter (e.g. 12/24 hour display formatting, GPS baud rate, etc.) is changed during this 20 msec loop, the Nano's EEPROM is updated with the new value.  These EEPROM values are used for system initialization upon power-up.

The second loop is a 1 second loop, and its tasks are:

  • Receive the NMEA-0183 message stream, strip off the time field, and write the time to the front panel display.
  • If displaying a menu item in lieu of time, update the menu's display with any changes to a menu's selection that occurred during the 20 msec loop.
  • Blink (if enabled) the display's "colon" between the hour and minutes fields.
  • Read ambient light level via the Nano's analog input A0.

Note that this code only  receives data from the GPS receiver, it does not transmit data back to it.  Therefore, to change any of the receiver's parameters, such as its baud rate (from its default of 9600 baud to, say, 38,400 baud), this change must be done using an external CH340 USB-to-Serial adapter and external software such as u-block's u-center (a very useful tool, by the way, for verifying if the receiver is working, if you are unsure of your Arduino code).

There are 9 possible menu items that can be displayed via the 4-digit front panel display.  These menu items can be stepped through using the momentary-contact MENU toggle switch.  Within each menu item there are usually a number of  choices available (such as different baud rates in the baud rate menu).  These choices can be stepped through using the NEXT pushbutton. 

The MENU items, and the range of possible choices for each MENU item, are shown, below:

  • Menu Item 1:
    • Displays current Time.
    • The NEXT button does not function for this menu item.
    • Display:

  • Menu Item 2:
    • Allows setting of the local time zone's offset, in hours, from GMT.  Note that the offset is per Standard Time, not Daylight Savings Time.  E.g. it would be -8 for California, irrespective of the month.
    • The NEXT button steps from 0 to -23, then wraps back around to 0.
    • Display

  • Menu Item 3:
    • Allows setting of the "Brightness" of the 4-digit LED display.
    • The NEXT button steps through the 4 possible brightness values, from 0 to 3, and then wraps back around to 0.
    • Display:

  • Menu Item 4:
    • Selects displaying time in a 12 or a 24 hour format.  "12" will be displayed if the 12-hour format is selected, and "24" will be displayed if the 24-hour format is selected.
    • The NEXT button toggles between the 12 and 24 hour selections.
    • Display:

  • Menu Item 5:
    • Determines if the colon dividing the 2-digit hours field from the 2-digit minutes field will blink (1 second ON followed by 1 second OFF), or if it is ON continuously.
    • The NEXT button toggles between the BLINK or ON states.
    • Display:

  • Menu Item 6:
    • Selects of one of five possible baud rates to use for the Nano's Software UART that communicates with the GPS Receiver.
    • The NEXT button steps through the following baud rates, and then wraps around to the start:
      • 9,600
      • 19,200
      • 38,400
      • 57,600
      • 115,200
    • Display:

  • Menu Item 7:
    • Displays the number of satellites tracked by the GPS receiver (used for debugging).
    • The NEXT button does not function for this menu item.
    • Display:

  • Menu Item 8:
    • Enables the display brightness to change as a function of the ambient light level, or keeps the display brightness fixed at the level set in Menu item 3, irrespective of the ambient light level.
    • The NEXT button toggles between enabling the display brightness to track ambient light levels, or the display brightness remaining fixed.
    • Display:

  • Menu Item 9:
    • Displays the Nano's ADC input value from the Ambient Light detector (useful for debugging).  Values range from 0 (dark) to 1023 (max light).
    • The NEXT button does not function for this menu item.
    • Display:


The MENU switch steps through the items from 1 to 9, and then will cycle back to 1. 

Similarly, the NEXT button steps through a menu item's possible values from first to last, and then cycles back to the first. 

Note that my GPS Clock Arduino code can be found here, on GitHub:  Arduino Clock Code.


Mechanical:

I used a small plastic box within which I mounted the electronics and the display.  I'd purchased this box (which apparently had been a shipping product) sometime in the dim-past, I'm sure because I thought I might someday be able to its case for a project.

For my new application of this case, I constructed both the front panel and the back panel from blank double-sided PCB stock.

The front panel simply has the time display and a small photoresistor, the latter is used to detect a room's ambient light level and with it, adjusts the display's brightness accordingly (i.e. max display brightness for a bright room, to minimum display brightness for a dark room).

The rear panel (see image below) has three switches, an SMA connector for the GPS antenna, and a hole to allow a mini-USB connector to be attached to the Nano within the box.

The three switches on the back panel are:

Leftmost switch:  3-position single pole toggle.  This switch selects between the following items:

  • UP:  Display time as Daylight Savings time (add an hour to Standard Time).
  • MID: Display time as Standard Time (no Daylight Savings Offset).
  • DOWN:  Display GMT time with 24-hour format.
Middle switch:  3-position momentary contact single pole toggle which is the MENU switch. This switch performs the following functions:
  • MID (default position): Idle
  • UP or DOWN (momentary contact):  Upon moving the switch to either its up or down position and then releasing it back to its idle (mid) position, the menu will increment to the next menu item on the list of menu items.  At the end of the list it will wrap around to the first menu item, which simply display's time.

Rightmost switch:  Normally Open push-button, called the "NEXT" switch.  Upon the press-and-release of this pushbutton, software will increment to the next value choice for the menu-item that has been selected..  For example, if in the "GPS Baudrate Select" menu and the current entry is 9600, upon the press-and-release of this pushbutton, the baud rate will increment to the next choice, in this case 19,200.  At the last choice the next press-and-release will wrap back around to the first choice.

The electronics are mounted on a small prototyping board:


This board, in turn, is mounted onto another piece of raw double-sided PCB stock that is large enough to let me attach it to the case's four mounting holes).




An overhead view of the final build:


Regarding the front-panel display, I found it unattractive that unilluminated segments of the display's numerals were visible from the front, so I mounted a small piece of "smokey" colored Gel Film between the display and the front panel to better hide any non-illuminated display segments.


Note that if using this type of Gel film, there is a protective layer on each side of the Gel film that will need to be peeled off.


Software Generation:

My wife suggested that I use ChatGPT to create the Arduino's code, so I thought, 'Why not', and gave it a try, wondering what the experience would be like.

I created a free account on the ChatGPT website and decided I would start by prompting ChatGPT to create code that would read and debounce two buttons (my MENU and NEXT switches).  Without any forehand knowledge on how best to use ChatGPT to write code, I plunged right in.

I quickly discovered that using the "Enter" key, in an attempt to format my request, immediately sends whatever I'd typed to ChatGPT, as you can see in the first line below.  My next attempt you can see right below it -- a long paragraph written "stream-of-consciousness" fashion.  But it worked!


I continued in the same vein, incrementally adding features and then testing them.  I was amazed at how quickly I was generating code with ChatGPT as a partner (although perhaps I should rephrase that as being amazed at how quickly ChatGPT was generating code with me as a partner).  I figured I'd have the complete sketch finished by the afternoon.

Almost, but not quite...ChatGPT is not without its flaws!

As ChatGPT continued to generate code as I added features or asked it to move things around, errors began to creep into already established code.

For example, when stepping through the menus, I usually defined the first two characters to be a shorthand ID for that menu item, e.g. 'br' would display for the "Brightness" menu.  And I specified for ChatGPT which display segments (e.g. segments E, F G, etc.) of the first two seven-segment numbers, to turn on for each menu item that a shorthand ID .

The resulting code looked great -- and when tested, the ID characters were properly displayed on the 4-digit seven-segment display, and I continued on my way, working on a different part of the code...

Later I went back to test some function that required I first step through the menus, and I noticed that for the menus for which I had defined ID characters for the first two digits, the letters had become gobbledygook.

Somehow, while I had ChatGPT adding other features to the code, ChatGPT had decided to replace the working "letters-defined-with-segments" code to instead define the letters with Hex values that it seemed to believe represented the same segments as I had specified.

Except these Hex values did not represent those same segments, as I show, below:

Returning the code to the original "define-with-segments" code fixed the problem, but it should never have occurred in the first place.

Another issue that took me a frustrating amount of time to debug had to do with switch-case statements.

In the Menu code, ChatGPT initially defined the menu selection tree using nested if-else statements.  I much prefer to use case statements, and I requested that ChatGPT change its code do so, as you can see by the following conversation.

First, ChatGPT's original code:


Next, my request to use Case statements and ChatGPT's reply...


...and the code it generated:


Looks good and it worked as expected.

Much later, during the verification of some other code that ChatGPT had added, I needed to step through my menus to get to the appropriate item to verify.  But as I stepped through them, I could not step from the Brightness menu to the next menu item.  What was going on?

After scratching my head for awhile, I finally throw up my hands in frustration and instead queried Google's AI as to what the problem might be.  Its reply:


Aha!  I looked at ChatGPT's code again -- it had declared a variable within a Case statement (it actually had done this in two case statements), as I show below.


I moved the two declarations to the top of the sketch to make it a global variable, not local, and the problem went away.

This isn't a problem I would have had if I'd written the code myself.  I usually define most of my variables as globals and place them at the top of my code, and whenever I do define local variables, they are at the start of a function call (i.e. subroutine), rather than in-line within the body of the code.

And because it's something I wouldn't do, myself, I didn't realize that it shouldn't be done.  Thank goodness for Google.

Anyway...by this time I decided that I was spending too much time fixing ChatGPT errors and I'd be better off completing the code myself.  And so I did.

In summary...

I'd estimate that the coding effort was 90% ChatGPT and 10% me.  With ChatGPT, it took about a full day to create my first fully-featured version of the code (Rev. 1).  I'm sure it would have taken me longer if I had had to write all of the code, myself.

In my opinion, ChatGPT is an incredibly useful aid for creating code -- I avoided all the usual typos I would normally generate (and would have to find) if I had been typing the code in.

But, surprise surprise, ChatGPT can create errors -- especially frustrating was its rewriting of already working code into failing code.

Also -- there's a certain "I know it in the core of my being" sense that I get from writing my own code.  If something isn't working properly, I know right where to start looking for the cause, because I'd thought about, and written, the code.

With ChatGPT's code I don't have the same sense of familiarity.  Which means that coding errors, if they exist, could be more difficult to uncover.


Other Issues with the Clock:

Incorrect Time (Infrequent):

With my first version of code, I noticed that sometimes (very infrequently), there'd be an error in the displayed, for example, time appearing as 5:22 when it was actually a different time of day.

Could this be a problem with the Nano's software serial port somehow losing data?

The GPS receiver's baud rate had been set to 9600 baud.  I hypothesized that NMEA 0183 data might be being dropped (corrupted) by the Nano's software serial port because other software tasks might have had temporary priority in lieu of serial port reception and decoding.

So I decided to bump up the baud rate from 9600 to 38,400 baud and see if the errors still occurred.  

I've seen no errors since the modification.  That seems to have fixed the problem.

Only 4 levels of Display Brightness:

Documentation suggests (or states?) that the TM1637 displays have 8 steps of brightness (0 to 0x7).  The displays that I purchased only seem to have 4 steps (0 to 0x3), and brightness seems to be unchanged when incrementing the brightness field from 0x3 to 0x7.  So I limited the brightness adjustment range to the four steps from 0 to 3.


That's all!


Standard Caveat:

As always, I might have made a mistake in my equations, assumptions, drawings, or interpretations.  If you see anything you believe to be in error or if anything is confusing, please feel free to contact me or comment below.

And so I should add -- this information is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.