A Raspberry Pi GPS project connects a UART GPS module to the Pi’s serial pins, runs GPSD to parse incoming NMEA sentences, and uses Python to log position data or build location-aware applications. The same setup with a PPS-capable GPS module makes the Pi a Stratum 1 NTP server with nanosecond-level time accuracy. This guide covers hardware selection, correct Bookworm wiring and software configuration, a working GPS logger in Python using the current gpsd-py3 library, GPS time synchronisation with chrony, and troubleshooting the common failure modes.
Last tested: Raspberry Pi OS Bookworm Lite 64-bit | May 2025 | Raspberry Pi 4 Model B (4GB) | NEO-6M GPS module, Adafruit Ultimate GPS Breakout (PPS), GPSD 3.25, chrony 4.3
Key Takeaways
- The
gps3Python library is deprecated and unmaintained since 2018. Do not install it. The current Python GPS libraries on Bookworm arepython3-gps(installed via APT, provides the systemgpsmodule) andgpsd-py3(installed via pip inside a virtual environment). Any guide usingfrom gps3 import gps3is outdated. - The UART serial device on Bookworm is
/dev/serial0, which aliases to the hardware UART. The Pi’s serial console must be disabled before GPSD can use/dev/serial0. Runsudo raspi-config, navigate to Interface Options, Serial Port, disable the login shell, and keep the hardware enabled. Without this step GPSD receives garbled data because the console and GPS share the port. - For GPS time synchronisation on Bookworm, use
chronyrather thanntp. For monitoring the Pi’s performance as a GPS server, see Grafana InfluxDB Raspberry Pi: Complete Monitoring Stack Setup Guide. Chrony is the default NTP implementation on Bookworm and provides better accuracy on GPS/PPS sources. The classicntppackage still works but requires manual installation and its configuration differs from chrony’s.
Hardware: Pi Models, Raspberry Pi GPS Modules, and Wiring
Any Pi model with a 40-pin header works for Raspberry Pi GPS projects. Pi Zero 2W is the best choice for portable battery-powered GPS loggers due to its low power draw. Pi 4 or Pi 5 suit desktop GPS processing, mapping applications, or NTP servers where sustained performance matters. The Raspberry Pi Pico and Pico 2 are microcontrollers and run MicroPython rather than Linux; GPSD does not run on them, but they can receive and parse NMEA sentences directly at a lower level.
GPS module selection. The NEO-6M is the most common entry-level module: approximately 2.5m accuracy, UART communication, 5Hz update rate, built-in antenna, and low cost. The Adafruit Ultimate GPS Breakout (MTK3333-based) provides 1.8m accuracy, a 10Hz update rate, and a PPS output pin for time synchronisation applications. The Beitian BN-220 is a compact module with multi-GNSS support suitable for drone or vehicle builds.
UART wiring for NEO-6M and most UART GPS modules: GPS VCC to Pi pin 1 (3.3V). Most modules accept 3.3V or 5V; verify the module datasheet before using 5V. GPS GND to Pi pin 6. GPS TXD to Pi pin 10 (RXD0, GPIO15). GPS RXD to Pi pin 8 (TXD0, GPIO14). The TXD and RXD cross: the module’s transmit goes to the Pi’s receive and vice versa. Swapping these is the most common wiring error.

For the PPS time synchronisation use case, the Adafruit Ultimate GPS Breakout provides a dedicated PPS output pin. Connect PPS to a spare GPIO pin (GPIO4 / pin 7 is conventional). Add a 10k pull-up resistor between PPS and 3.3V for clean signal edges.
Enable UART and disable the serial console. On Pi 4, edit /boot/config.txt. On Pi 5, edit /boot/firmware/config.txt. Add or verify:
enable_uart=1
Then disable the serial login shell:
sudo raspi-config
# Interface Options > Serial Port
# Login shell: No
# Serial port hardware: Yes
sudo reboot
Expected result: After reboot, sudo cat /dev/serial0 shows a stream of NMEA sentences beginning with $GP or $GN. If nothing appears, check TXD/RXD wiring. If garbled text appears, the console was not disabled; re-run raspi-config and confirm the login shell is disabled.
Software Setup: GPSD and Python on Bookworm
Install GPSD and the Python GPS library. Add the current user to the dialout group for persistent serial port access without sudo:
sudo apt update && sudo apt full-upgrade -y
sudo apt install -y gpsd gpsd-clients python3-gps
sudo usermod -aG dialout $USER
Configure GPSD to use the hardware serial port. Edit /etc/default/gpsd:
DEVICES="/dev/serial0"
GPSD_OPTIONS="-n"
START_DAEMON="true"
USBAUTO="false"
The -n flag tells GPSD to start listening immediately rather than waiting for a client connection. Restart GPSD:
sudo systemctl restart gpsd
sudo systemctl enable gpsd
Test with the cgps client:
cgps -s
Expected result: cgps displays a panel with latitude, longitude, altitude, speed, and satellite count. The fix status shows 3D Fix once the module has locked onto sufficient satellites. First fix outdoors takes 1 to 5 minutes on a cold start. If cgps shows no data, check systemctl status gpsd and verify /dev/serial0 is listed as the active device.
For Python scripts, install gpsd-py3 inside a virtual environment on Bookworm:
python3 -m venv ~/gps-env
source ~/gps-env/bin/activate
pip install gpsd-py3
A basic script to print the current position:
import gpsd
import time
gpsd.connect()
while True:
packet = gpsd.get_current()
if packet.mode >= 2:
print(f"Lat: {packet.lat:.6f} Lon: {packet.lon:.6f} "
f"Alt: {packet.alt:.1f}m Speed: {packet.hspeed:.1f}m/s")
else:
print("Waiting for fix...")
time.sleep(1)
packet.mode returns 1 (no fix), 2 (2D fix, no altitude), or 3 (3D fix with altitude). Check for mode >= 2 before reading coordinates to avoid exceptions on startup before the first fix.
Alternatively, read NMEA sentences directly from the serial port without GPSD using pyserial inside the virtual environment. This is useful when GPSD is not needed and minimal overhead is preferred:
pip install pyserial
import serial
ser = serial.Serial('/dev/serial0', baudrate=9600, timeout=1)
while True:
line = ser.readline().decode('ascii', errors='replace').strip()
if line.startswith('$GPGGA') or line.startswith('$GNGGA'):
parts = line.split(',')
if parts[2] and parts[4]:
print(f"Raw NMEA: {line}")
Raspberry Pi GPS Logger and Data Visualisation
A GPS logger records position, altitude, speed, and timestamp at regular intervals to a CSV file for later analysis. This is useful for hiking route recording, vehicle tracking, drone flight logging, and weather balloon payloads. The script below uses gpsd-py3 and writes to CSV:
import gpsd
import csv
import time
from datetime import datetime
gpsd.connect()
with open('/home/pi/gps_log.csv', 'a', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['timestamp', 'lat', 'lon', 'alt_m', 'speed_ms', 'satellites'])
while True:
packet = gpsd.get_current()
if packet.mode >= 2:
writer.writerow([
datetime.utcnow().isoformat(),
round(packet.lat, 7),
round(packet.lon, 7),
round(packet.alt, 1) if packet.mode == 3 else '',
round(packet.hspeed, 2),
packet.sats
])
csvfile.flush()
time.sleep(5)
Run the logger as a systemd service for unattended logging. Replace youruser with the username set at flash time:
sudo tee /etc/systemd/system/gps-logger.service <<EOF
[Unit]
Description=GPS Logger
After=gpsd.service
Requires=gpsd.service
[Service]
User=youruser
WorkingDirectory=/home/youruser
ExecStart=/home/youruser/gps-env/bin/python3 /home/youruser/gps_logger.py
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now gps-logger
Expected result: systemctl status gps-logger shows active (running). The CSV file grows with one row per 5-second interval. Rows without a fix have empty altitude and speed fields. To convert the CSV to GPX for Google Earth or mapping software, use GPSBabel: sudo apt install -y gpsbabel, then gpsbabel -i unicsv -f gps_log.csv -o gpx -F gps_log.gpx. The resulting GPX file imports into Google Earth, QGIS, or OsmAnd.
Time Synchronisation with PPS and Chrony
A GPS module with a PPS output delivers a precise one-pulse-per-second signal synchronised to GPS satellite atomic clocks. Connected to a GPIO pin and combined with GPSD, this makes the Pi a Stratum 1 NTP server: a primary time source that distributes accurate time to the local network. Chrony is the correct NTP implementation on Bookworm. It handles GPS and PPS sources natively and is more accurate than the classic ntp package on edge sources.
Enable the PPS GPIO overlay. On Pi 4, edit /boot/config.txt; on Pi 5, edit /boot/firmware/config.txt. Add:
dtoverlay=pps-gpio,gpiopin=4
Install the PPS tools and chrony:
sudo apt install -y pps-tools chrony
sudo reboot
After reboot, verify the PPS device is recognised:
ls /dev/pps*
sudo ppstest /dev/pps0
Expected result: /dev/pps0 exists and ppstest outputs a line per second with an assertion timestamp. If /dev/pps0 does not exist, check that the PPS wire is connected to GPIO4, the overlay is in the correct config.txt path, and the reboot applied the change.
Configure chrony to use both GPSD shared memory and the PPS device. Edit /etc/chrony/chrony.conf and add these lines:
# GPS via GPSD shared memory (SHM0 = time, SHM1 = PPS)
refclock SHM 0 offset 0.5 delay 0.2 refid GPS noselect
refclock SHM 1 offset 0.0 delay 0.2 refid PPS
# PPS device directly
refclock PPS /dev/pps0 refid PPSD precision 1e-7 prefer lock GPS
Restart chrony:
sudo systemctl restart chrony
chronyc sources -v
Expected result: chronyc sources shows GPS and PPS sources with * or + markers indicating active synchronisation. The PPS source should show offset in the nanosecond range once the GPS module has a stable fix. A freshly started GPS module needs 5 to 15 minutes to stabilise. For devices on the local network to use this Pi as their NTP server, add allow 192.168.1.0/24 to chrony.conf and restart.
Troubleshooting Raspberry Pi GPS
No NMEA output from cat /dev/serial0. Either the serial console is still active on the port, the TXD/RXD wires are swapped, or the module is not receiving power. Disable the console via raspi-config as described in the hardware section. Verify wiring: the GPS module’s TXD goes to Pi pin 10 (RXD), not pin 8. Confirm the module’s power LED is lit. Some modules have a status LED that blinks once per second after acquiring a satellite fix.
GPSD shows no fix or “waiting for signal”. A GPS module needs an unobstructed view of the sky. Indoors or near windows, fix acquisition may take 5 to 10 minutes on a cold start. The module’s fix LED (on NEO-6M and similar) blinks every 5 seconds before a fix and every second after. If cgps shows satellites at 0/0, the antenna is not receiving signal. Move outside or connect an external active antenna. Check cgps -s for signal-to-noise ratios on individual satellites; anything below 20 dB-Hz will not contribute to a fix.
Python script fails with connection refused or no data. GPSD must be running before the Python script connects. Check systemctl status gpsd. Verify the GPSD socket is active with ls /var/run/gpsd.sock. The -n flag in GPSD_OPTIONS ensures GPSD listens without waiting for a client. If that flag is absent, GPSD may not have polled the GPS port before the Python script tries to read. If using python3-gps system library, ensure the script imports import gps not from gps3 import gps3.
Serial port access denied. The user must be in the dialout group. Run groups and confirm dialout appears. If not, run sudo usermod -aG dialout $USER and log out and back in. Using sudo chmod 666 /dev/serial0 as a workaround resets on reboot and is not the correct fix. The group membership approach is permanent.
PPS device absent after enabling the overlay. Verify the overlay line is in the correct config.txt path for the Pi model (Pi 4: /boot/config.txt, Pi 5: /boot/firmware/config.txt). Check dmesg | grep pps after reboot for kernel messages about PPS initialisation. If the module’s PPS output is floating (not connected to GPIO4), the kernel sees the device but ppstest times out. Confirm the physical wire from the module’s PPS pad to Pi GPIO4 is seated.
Inaccurate position readings. Urban canyons (tall buildings on multiple sides), dense tree cover, and indoor operation all reduce accuracy. Enable SBAS/WAAS in the GPS module’s configuration using ubxtool for u-blox modules. This adds satellite-based augmentation corrections that improve accuracy to under 1m in covered regions. Averaging multiple readings over time reduces noise for stationary applications. For applications requiring centimetre-level accuracy, RTK GPS with a local base station is the correct technology; standard GPS modules cannot achieve this. For IoT sensor projects that combine GPS with other sensors over MQTT, see Raspberry Pi IoT Projects: Sensors, MQTT, and Real Builds.
FAQ
Which GPS module is best for Raspberry Pi?
For general GPS logging projects, the NEO-6M is the most cost-effective starting point. For better accuracy and multi-GNSS coverage, the u-blox M8N is worth the slightly higher price. For time synchronisation as a Stratum 1 NTP server, use the Adafruit Ultimate GPS Breakout specifically because it provides a PPS output. For drone or vehicle builds where compact size matters, the Beitian BN-220 is a well-regarded multi-GNSS option. All four communicate over UART at 9600 baud by default and work with GPSD on Bookworm.
How do I read GPS data in Python on Bookworm?
Install GPSD system-wide with sudo apt install -y gpsd python3-gps, then either use the system gps module or install gpsd-py3 via pip inside a virtual environment. The gps3 library is deprecated since 2018 and should not be used. The simplest approach for new code is gpsd-py3: import gpsd; gpsd.connect(); packet = gpsd.get_current(). For direct serial parsing without GPSD, use pyserial inside a venv to read raw NMEA sentences from /dev/serial0.
Can a Raspberry Pi be used as a GPS NTP time server?
Yes. A GPS module with PPS output connected to a GPIO pin, combined with GPSD and chrony on Bookworm, creates a Stratum 1 NTP server. Chrony uses the GPS time from GPSD’s shared memory interface and the PPS signal for sub-microsecond accuracy. Other devices on the local network point their NTP configuration at the Pi’s IP address. The Adafruit Ultimate GPS Breakout is the standard module for this use case because its PPS output is well-documented and reliable.
How do I improve GPS accuracy on Raspberry Pi?
Four practical approaches in order of impact. First, ensure the antenna has clear sky visibility. Second, use a u-blox M8N or similar multi-GNSS module that receives GPS, GLONASS, and Galileo simultaneously for more satellites in view. Third, enable SBAS/WAAS in the module’s configuration (ubxtool for u-blox modules) to apply satellite-based correction signals that can reduce error to under 1m. Fourth, average multiple readings over time for stationary applications. RTK GPS provides centimetre accuracy but requires a separate base station and significantly higher hardware cost.
Why is the serial port busy or showing garbled GPS output?
The most common cause is the serial console still being active on /dev/serial0. By default on Bookworm, the Pi’s UART is assigned to the system console. Both the console and GPSD trying to use the same port produces garbled output. Fix it by running sudo raspi-config, navigating to Interface Options, Serial Port, disabling the login shell, keeping the hardware serial port enabled, and rebooting. After this change, cat /dev/serial0 shows clean NMEA sentences.
References:
- GPSD documentation: gpsd.gitlab.io/gpsd
- gpsd-py3 library: github.com/MartijnBraam/gpsd-py3
- chrony documentation: chrony-project.org/documentation
- u-blox ubxtool reference: gpsd.gitlab.io/gpsd/ubxtool
- GPSBabel: gpsbabel.org
About the Author
Chuck Wilson has been programming and building with computers since the Tandy 1000 era. His professional background includes CAD drafting, manufacturing line programming, and custom computer design. He runs PidiyLab in retirement, documenting Raspberry Pi and homelab projects that he actually deploys and maintains on real hardware. Every article on this site reflects hands-on testing on specific hardware and OS versions, not theoretical walkthroughs.
Last tested hardware: Raspberry Pi 4 Model B (4GB), NEO-6M GPS module, Adafruit Ultimate GPS Breakout. Last tested OS: Raspberry Pi OS Bookworm Lite 64-bit. GPSD 3.25, chrony 4.3.

