Skip to content

cyberlord8/NTPServer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

69 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation


Note (🤖✨AI-Generated README): This README.md was generated with AI assistance. It may contain mistakes, outdated assumptions, or wording that’s overly enthusiastic. Treat it as a starting point and verify details (pin mappings, build steps, features) against the actual code and hardware.

I've gone through the README.md file a few times now and I believe I have corrected all the AI hallucinations/errors.


Pico W GPS NTP Server

A GPS-backed NTP server for the Raspberry Pi Pico W (RP2040), built in C++ with the Pico SDK.

The project currently uses NMEA data from a GPS receiver on UART0 to establish UTC time, uses PPS edge timing to improve second-boundary alignment, serves NTP on UDP/123 over Pico W Wi-Fi, and presents a live ANSI status dashboard over USB CDC. The codebase also includes support for an HT16K33 4-digit I2C clock display, though that display update path is currently disabled in main.cpp.


Current status

The project is beyond the original “NMEA-only prototype” stage.

What is implemented now:

  • GPS UART input on uart0 using an IRQ-driven ring buffer
  • Lightweight NMEA parsing for RMC, GGA, and ZDA
  • GPS device state machine with Booting, Acquiring, Acquired, Locked, and Error
  • PPS input capture by GPIO interrupt using time_us_64() timestamps
  • Timebase seeding from GPS UTC and PPS-assisted second alignment
  • NTP server on UDP port 123 using lwIP
  • Pico W Wi-Fi station mode with configurable static IPv4 support
  • ANSI single-screen dashboard over USB serial
  • LED state indication driven from a repeating timer
  • Internal temperature readout with EMA smoothing
  • HT16K33 I2C clock display support in the tree

What is still in progress or not fully finished:

  • Tight PPS discipline and long-term holdover behavior still need refinement
  • The code reports synchronized state aggressively in places and should be tightened further as the timing model matures
  • The HT16K33 display update function exists, but the call is commented out in the main loop, so the external clock is not presently being refreshed
  • USB Ethernet / composite USB work is not part of the current active firmware path

Hardware target

MCU

  • Raspberry Pi Pico W

GPS

  • Waveshare Pico GPS L76X, or another GPS receiver that provides NMEA output

Current default wiring in code

GPS on UART0

  • GPS TX -> Pico GPIO1 (UART0 RX)
  • GPS RX -> Pico GPIO0 (UART0 TX)
  • GPS GND -> Pico GND
  • GPS VCC -> Pico 3V3 or module-appropriate supply

PPS

  • GPS PPS -> Pico GPIO16

Optional HT16K33 clock display

  • I2C1 SDA -> GPIO14
  • I2C1 SCL -> GPIO15
  • Default I2C address: 0x70

Adafruit I2C Clock Display Wiring

The project includes support for an HT16K33-based 4-digit Adafruit I2C clock display.

Adafruit 0.56" Seven-Segment Backpack Guide

https://learn.adafruit.com/adafruit-led-backpack/0-dot-56-seven-segment-backpack

Current Pin Mapping in Code

The current code initializes the display like this:

ht16k33_clock_init(i2c1, 14, 15, 0x70, 100000);

That means the firmware is using:

  • I2C instance: i2c1
  • Pico GP14: SDA
  • Pico GP15: SCL
  • I2C address: 0x70
  • I2C speed: 100 kHz

Wiring

Connect the Adafruit display to the Pico / Pico W as follows:

Pico Pin Function Adafruit Display Pin
GP14 I2C1 SDA SDA
GP15 I2C1 SCL SCL
3V3(OUT) 3.3V power VCC
GND Ground GND

Notes

  • The code enables the Pico’s internal pull-ups on SDA and SCL.
  • The display is currently configured for the default HT16K33 I2C address 0x70. If the backpack address jumpers are changed, update the address in main.cpp.
  • This display path is intended for an I2C HT16K33 backpack, not a TM1637 display.
  • Use a display module that is compatible with 3.3V I2C logic.
  • If using a module that has 5V logic then supply the HT16K33 backpack with 5V power.
  • In the current source, the clock display initialization is present, but the periodic update call may still be disabled in the main loop depending on your branch, so wiring it correctly does not necessarily mean it will update until that call is enabled.

Example Summary

Pico GP14 -> SDA Pico GP15 -> SCL Pico 3V3(OUT) -> VCC Pico GND -> GND

Optional Address Reminder

If you ever change the HT16K33 address jumpers on the Adafruit board, update this line accordingly:

ht16k33_clock_init(i2c1, 14, 15, 0x70, 100000);
                                 ^^^^ - Change this address

Software architecture

Main loop

main.cpp performs the following high-level flow:

  1. Initialize USB stdio
  2. Initialize temperature, uptime, timebase, and optional clock display support
  3. Initialize Wi-Fi and start the NTP server if Wi-Fi connects successfully
  4. Initialize LED state binding and repeating LED timer
  5. Initialize GPS UART on uart0
  6. Initialize PPS capture on GPIO16
  7. Enter the main loop to:
    • pull NMEA lines from the UART ring buffer
    • update GPS state
    • feed new PPS edges into the timebase
    • service the LED output
    • redraw the dashboard

The dashboard is currently active. The HT16K33 display update call is present but commented out.


GPS and state handling

UART reception

gps_uart.cpp implements an IRQ-driven receive path with a ring buffer and line extraction helper. The code is designed to avoid blocking the main loop while still assembling full NMEA lines.

NMEA parsing

gps_state.cpp currently handles:

  • RMC for UTC time/date validity and timebase seeding
  • GGA for fix quality, satellites, and HDOP
  • ZDA for a formatted UTC timestamp string shown on the dashboard

The parser intentionally ignores unrelated sentences instead of letting them affect fix state. Acquired presently requires both:

  • valid RMC status (A)
  • valid GGA fix quality

Locked is entered only when PPS is present, recent, and approximately 1 Hz.

GPS states

The current state model is:

  • Booting
  • Acquiring
  • Acquired
  • Locked
  • Error

In practical terms:

  • Acquiring means GPS time/fix conditions are not yet sufficient
  • Acquired means valid NMEA-derived time and fix are present
  • Locked means PPS is being detected and passes the present sanity checks

PPS and timebase

PPS capture

pps.cpp arms a rising-edge IRQ on the configured GPIO and records:

  • total edge count
  • last PPS interval in microseconds
  • absolute time of the last edge in microseconds since boot

This is already live in the current code, not just planned.

Timebase behavior

timebase.cpp maintains a baseline of:

  • UTC Unix seconds
  • local microsecond counter at the baseline instant

It can be seeded from GPS UTC and then queried as either Unix time or NTP seconds/fraction. The current implementation also tracks PPS edges and uses fresh PPS edges to improve second-boundary alignment when GPS UTC updates arrive. When PPS steps are accepted, the baseline can advance on PPS edges as well.

This means the project is no longer purely “NMEA timestamp only,” but it is also not yet a fully mature disciplined clock in the chrony/ntpd sense.


NTP server behavior

ntp_server.cpp implements a minimal server using lwIP UDP.

Current behavior:

  • listens on UDP port 123
  • responds only to NTP client mode (mode 3) requests
  • copies the request packet payload into a packed NtpPacket structure
  • populates receive and transmit timestamps from the internal timebase
  • echoes the client transmit timestamp back into originate timestamp
  • uses "GPS\0" as the reference ID

Current stratum behavior:

  • 1 when time is available and the timebase reports synced
  • 2 when time is available but not synced
  • 16 when no usable time is available

Leap indicator behavior:

  • 0 when synced
  • 3 when unsynchronized/alarm

This logic is already implemented and active.


Wi-Fi behavior

wifi_cfg.cpp initializes CYW43, enables STA mode, and supports either static IPv4 or DHCP behavior. The current main.cpp path configures static addressing before connecting. The IP octets are sourced from W_IPAddress, while subnet mask, gateway, and optional DNS are assigned in code.

The existing README’s hard-coded static IP description was too specific for the present source tree unless W_IPAddress is set to that exact value in headers not shown here.


Console dashboard

ui_console.cpp renders a single-screen ANSI dashboard refreshed every 500 ms. It currently shows:

  • GPS state
  • RMC validity
  • GGA fix status
  • satellite count
  • HDOP
  • UTC from ZDA
  • UTC from RMC time field
  • PPS detection, interval, age, and timebase PPS telemetry
  • CPU temperature
  • uptime
  • Wi-Fi link state and IP address
  • NTP server up/down state and port

This is more advanced than the earlier README indicated because PPS diagnostics are already present in the UI.


LED behavior

led.cpp uses a 50 ms repeating timer callback to compute desired LED state and a foreground led_service() function to apply it. Patterns differ by GPS state.

That structure keeps the timer callback short and avoids doing unnecessary heavy work in timing-sensitive paths.


Optional HT16K33 clock display

The repository now contains ht16k33_clock.cpp, which provides support for a 4-digit I2C clock display using an HT16K33 backpack. It supports:

  • initialization on a selected I2C instance
  • brightness control
  • clear / dashed fallback display
  • displaying HH:MM from Unix UTC time with optional UTC offset

In main.cpp, the clock is initialized, but update_clock_display() is currently commented out in the main loop, so this feature is not presently active at runtime.


Repository layout

  • main.cpp - system bring-up and main loop
  • gps_uart.{h,cpp} - GPS UART IRQ receive path and line extraction
  • gps_state.{h,cpp} - NMEA parsing and GPS state machine
  • pps.{h,cpp} - PPS GPIO IRQ capture and telemetry
  • timebase.{h,cpp} - UTC baseline, PPS edge integration, NTP time conversion
  • ntp_server.{h,cpp} - lwIP UDP NTP server
  • wifi_cfg.{h,cpp} - CYW43 Wi-Fi initialization and IP configuration
  • ui_console.{h,cpp} - ANSI dashboard
  • led.{h,cpp} - LED patterns and LED service logic
  • temp.{h,cpp} - RP2040 internal temperature reading and smoothing
  • ht16k33_clock.{h,cpp} - optional 4-digit I2C clock display support
  • uptime.{h,cpp} - uptime formatting helper

Build

Requirements

  • Pico SDK
  • CMake
  • Ninja
  • ARM embedded toolchain
  • PICO_BOARD=pico_w

Build commands

mkdir -p build
cd build
cmake -G Ninja .. -DPICO_BOARD=pico_w
ninja

Flash

Put the Pico W into BOOTSEL mode and copy the generated UF2 to the mounted mass-storage device.


Opening the Project in VS Code / VSCodium

This project can be opened directly as a normal CMake-based Pico SDK project in either VS Code or VSCodium. vscodium Screenshot

Prerequisites

Before opening the project, make sure the following are already installed and working on your system:

  • VS Code or VSCodium
  • CMake
  • Ninja
  • ARM GCC toolchain (arm-none-eabi-gcc / arm-none-eabi-g++)
  • Pico SDK
  • A working environment variable for the Pico SDK, typically:
export PICO_SDK_PATH=/path/to/pico-sdk

Open the Project Folder

Start VS Code or VSCodium, then open the folder that contains the project CMakeLists.txt.

From the menu:

  • File -> Open Folder
  • Select the root project folder
  • Open it

Or from a terminal:

codium .

or

code .

Let CMake Configure the Project

When the folder opens, VS Code / VSCodium should detect that this is a CMake project.

If prompted:

  • Allow CMake to configure the project
  • Select the appropriate kit/toolchain if needed
  • Make sure the target board is set for Pico W

This project is intended to build with:

-DPICO_BOARD=pico_w

Recommended Extensions

These extensions are commonly useful:

  • CMake Tools
  • C/C++
  • GitLens (optional)
  • Pico extension if you are using the Raspberry Pi Pico VS Code tooling

Build the Project

Once the project is open and configured, you can build it from the terminal:

mkdir -p build
cd build
cmake -G Ninja .. -DPICO_BOARD=pico_w
ninja

Or use the VS Code / VSCodium CMake controls to configure and build from the UI.

Important Note About New Source Files

If you add new .cpp files to the project, VS Code / VSCodium will not automatically add them to the build.

You must update CMakeLists.txt and add the new source file to the target source list manually.

Flashing

After a successful build, the generated .uf2 file can be copied to the Pico while it is in BOOTSEL mode.

Typical Workflow

A normal workflow in VS Code / VSCodium looks like this:

  1. Open the project folder
  2. Let CMake configure
  3. Build the project
  4. Connect the Pico in BOOTSEL mode to flash new firmware
  5. Open a serial terminal to view USB CDC output

Example serial console:

picocom -b 115200 /dev/ttyACM0

Troubleshooting

  • If the project does not configure, verify PICO_SDK_PATH
  • If headers are missing, confirm the Pico SDK and toolchain are installed correctly
  • If new source files are not compiling, check that they were added to CMakeLists.txt
  • If the board is wrong, make sure the build is using PICO_BOARD=pico_w

Wi-Fi secrets

The firmware expects a local wifi_secrets.h that defines at least:

#pragma once

inline constexpr const char WIFI_SSID[] = "YOUR_WIFI_SSID";
inline constexpr const char WIFI_PASSWORD[] = "YOUR_WIFI_PASSWORD";

Do not commit that file.


Running the console

The firmware uses USB CDC stdio. On Linux, for example:

picocom -b 115200 /dev/ttyACM0

You should see the dashboard refresh at about 2 Hz.

Dashboard Screenshot


Using the NTP server

Once Wi-Fi is up and the dashboard shows an IP address, the Pico W listens on UDP port 123.

Example quick test on Linux:

ntpdate -q <PICO_IP>

Example continuous test loop:

while true; do ntpdate -q <PICO_IP>; sleep 5; done

Dashboard Screenshot


Important status note on “Stratum 1”

The code is now PPS-aware, but the project should still be described carefully.

It is reasonable to say the firmware is a GPS/PPS-backed NTP server prototype or early Stratum-1 implementation. It is less accurate to describe it as fully finished Stratum-1 timing infrastructure at this point, because the timing discipline and sync qualification logic still need additional tightening.

In particular:

  • PPS capture is implemented
  • PPS affects lock state and timebase behavior
  • NTP stratum is driven by internal sync state
  • further refinement is still needed before calling the timing model fully mature

Known limitations and cleanup items

  • main.cpp currently uses sleep_ms() during startup for USB enumeration convenience; that is fine at boot but should remain outside timing-sensitive callbacks
  • the current sync logic in timebase.cpp is still evolving and should be reviewed as PPS discipline is tightened
  • the current LED source file appears to contain a likely typo in the non-CYW43 path (g_led_desired instead of s_led_desired), which should be checked in the full project build context before release documentation claims that file is final fileciteturn0file3
  • the clock display support exists but is not yet enabled in the main loop
  • gateway and DNS values are presently hard-coded in main.cpp

Near-term roadmap

  • tighten PPS discipline and sync qualification
  • decide the precise meaning of Acquired versus Locked for NTP stratum reporting
  • enable and validate the HT16K33 display path
  • make network configuration more flexible
  • continue toward USB Ethernet / composite USB options in future variants

Source basis for this README update

This update was based on the currently uploaded source files, including README.md, main.cpp, gps_state.cpp, gps_uart.cpp, ht16k33_clock.cpp, led.cpp, ntp_server.cpp, pps.cpp, timebase.cpp, ui_console.cpp, and wifi_cfg.cpp.

About

Raspberry Pi Pico based GPS Stratum 1 NTP Server

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors