When building timekeeping projects with microcontrollers, the biggest challenge isn’t displaying the numbers—it is maintaining accuracy. Real-time clock (RTC) modules like the DS3231 are excellent, but they eventually drift.
To achieve absolute atomic accuracy, I built a dual-display GPS Clock utilizing an Arduino Nano, a Neo-6M GPS receiver, a MAX7219 7-segment/matrix display for large time readouts, and an SSD1306 OLED screen for real-time telemetry.
While the concept is straightforward, implementing it on a low-RAM microcontroller like the ATmega328P (Arduino Nano) required solving several serial buffer and memory constraints. Here is the technical breakdown of the architectural details that make this build robust.
1. Silent Window I2C Synchronization
On the Arduino Nano, the SoftwareSerial library utilizes
an interrupt-driven RX buffer that is only 64 bytes
deep. The Neo-6M GPS module outputs NMEA sentences at 9600 baud, which
floods the serial line with characters.
When the Arduino updates the SSD1306 OLED over I2C, it is blocked for a few milliseconds. If a burst of GPS data arrives while the processor is busy writing I2C frames, the 64-byte buffer overflows instantly, corrupting NMEA checksums and causing time/date synchronization lags.
To prevent this, the firmware uses Silent Window Sync: 1. The Neo-6M module updates once per second (1 Hz). It transmits all sentences in a rapid burst (~200ms). 2. The remaining ~800ms of the second is an idle window where no serial data is transmitted. 3. The firmware detects when NMEA sentences stop arriving, and performs OLED updates strictly during this silent window.
This prevents overlapping I2C writes with active serial reception, resulting in zero dropped GPS packets.
2. Split-Tier Navigation (2D vs. 3D Fixes)
Cheap ceramic GPS antennas can struggle to get strong satellite fixes indoors or in urban areas.
To deal with weak antenna states, the telemetry parser implements a split-tier update rule: * 2D Navigation (3 Satellites): Updates Latitude, Longitude, and Speed. Horizontal position is calculated as soon as a 2D fix is achieved. * 3D Navigation (4+ Satellites): Locks and updates Altitude. Because altitude measurements are highly sensitive to geometry, altitude updates are gated by a 4-satellite requirement. This prevents the display from showing wild vertical drifts (e.g., jumping +/- 50 meters) when signal is weak.
3. Defense Against “Null Island” Glitches
During initial boot or when the GPS antenna recovers from signal
loss, the Neo-6M will occasionally output coordinates of exactly
0.0000, 0.0000 (often referred to as Null Island).
The code implements a sanity filter:
// Null Island / Zero-Coordinate Defense (Reject anything < 0.001 degrees)
if (abs(gps.location.lat()) > 0.001 || abs(gps.location.lng()) > 0.001) {
telemetry.lat = gps.location.lat();
telemetry.lon = gps.location.lng();
}This defense filters out coordinate glitches during cold starts and ensures garbage data is never written to the screen.
4. Persistent Location Memory
If you carry the GPS clock indoors or walk under a thick concrete roof, the satellite count drops to zero. Rather than displaying blank lines or resetting telemetry fields to zero, the display elegantly freezes on the last known good location.
This is done by separating transient fix indicators (like
telemetry.hasFix) from the coordinate variables. Only
valid, active updates overwrite the variables, providing persistent
location memory when signal is lost.
5. Advanced Display Caching
Transmitting data to the MAX7219 display over SPI on every iteration
of loop() is inefficient. The firmware maintains an active
digit cache array:
uint8_t lastMaxDigits[8] = {255, 255, 255, 255, 255, 255, 255, 255};During each loop, the new time digits are compared against the cache. The firmware only writes SPI commands to the MAX7219 for digits that have actually changed. This saves valuable processor clock cycles and reduces electrical noise in the circuit.
6. Anti-Freeze Fallback
A common issue with GPS clocks is that they freeze or stutter when entering a tunnel or losing signal.
To address this, the firmware uses the Arduino’s internal crystal
oscillator. Once a valid GPS time sync has occurred, the system
calculates time offsets based on millis(). If the GPS
stream drops out, the internal clock takes over seamlessly and ticks
forward. As soon as the GPS signal recovers, it resynchronizes with
atomic time, preventing freezes.
The Circuit Wiring
+-------------------+
| |
| ARDUINO NANO |
| |
+--------->| 5V |
| +------->| GND |
| | | |
| | | D3 (RX) <------------- TX (GPS Neo-6M)
| | | D4 (TX) -------------> RX (GPS Neo-6M)
| | | |
| | | A4 (SDA) <-----------> SDA (SSD1306 OLED)
| | | A5 (SCL) <-----------> SCL (SSD1306 OLED)
| | | |
| | | D5 (MOSI) -----------> DIN (MAX7219)
| | | D6 (SS) -----------> CS (MAX7219)
| | | D7 (SCK) -----------> CLK (MAX7219)
| | +-------------------+
Technical Summary
By managing state transitions, synchronizing display refreshes to NMEA silent periods, and caching SPI changes, the GPS Clock maintains absolute precision and reliability under hardware constraints.
If you want to build your own, check out the source code in the gpsclock repository!