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.
The Build:
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 use 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 Arduino Nano 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, in an attempt to format my request, that pressing "Enter" 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 without formatting for readability.
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 sketch completely finished by the
afternoon.
Almost, but not quite...ChatGPT is not without its flaws!
As ChatGPT continued to generate code with my descriptions of new features or requests to
move blocks of code 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 (see image, below). I had defined for ChatGPT which specific 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 by stepping through the menus, the ID characters for each menu item were properly displayed on the 4-digit seven-segment display.
Great! I continued on my way, assuming the code for that task was finished.
Later I went back to test a menu function, and, while stepping through the menus, I discovered the ID
characters had become gobbledygook.
Somehow, while I was asking ChatGPT to add 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.
A perfectly acceptable change to make, except...the Hex values chosen by ChatGPT 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. For purposes of readability, I much prefer to use case statements, and
I requested that ChatGPT change its code do use them, 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, as I show below.
I moved the declaration 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. Unless I'm stretched for memory, 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
further into the body of a function's 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 code generation 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 this respect, ChatGPT is an incredibly useful aid for creating code -- I
avoided all the usual typos I would normally generate if I had been typing the code in.
But, surprise surprise, ChatGPT can create errors. This is especially worrisome to me, if ChatGPT decides, unprompted, to change working code into something that no longer works. Sounds like a debugging nightmare!
Also -- there's a certain "I know it like the back of my hand" 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 am intimately familiar with the code and how it operates.
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. If it reoccurs, I'll do some more debugging (probably assign an I/O pin or two to be high during certain routines, thus allowing me to measure, with a scope, how much time they take to perform).
Only 4 levels of Display Brightness:
Documentation suggested that the TM1637 displays have 8 steps
of brightness (i.e. values 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!
P.S. For another of my Arduino GPS Clock designs, go here: Another Arduino GPS Clock.
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.