Project Overview & Difficulty Rating

To connect a reliable GPS for Raspberry Pi 5, use a UART-compatible module like the VK-162 (based on the u-blox M8 chipset) wired to GPIO 14 (TXD) and GPIO 15 (RXD). You must configure the primary UART via the uart0 overlay in the Bookworm OS firmware config, disable the Bluetooth UART conflict, and parse the NMEA sentences using the gpsd daemon alongside Python's gps3 library.

Project Spec Sheet
Difficulty: Intermediate (Requires Linux CLI and GPIO wiring)
Time to Complete: 45 minutes
Target Board: Raspberry Pi 5 (8GB variant tested)
OS Target: Raspberry Pi OS Bookworm (64-bit)
Estimated Cost: $85 ($73 for Pi 5 8GB + $12 for VK-162 module)

Unlike plug-and-play USB dongles, wiring a raw UART GPS module directly to the Pi's GPIO header gives you lower latency, frees up a USB 3.0 port for high-speed peripherals, and draws significantly less idle current. However, the Raspberry Pi 5 introduced subtle changes to the device tree and firmware paths compared to the Pi 4, which trips up many legacy tutorials. This guide uses the exact paths and overlays required for the Pi 5 architecture.

Hardware Spec Sheet & Module Comparison

Before soldering or crimping jumper wires, you need to select the right receiver. The market is flooded with counterfeit u-blox chips, so buying from reputable distributors is critical. Below is a data-dense comparison of the four most common modules used in Pi-based tracking projects in 2026.

Module Chipset Price (2026) Tracking Sensitivity Max Update Rate Default Baud
VK-162 (G-Mouse) u-blox M8 $12 - $15 -162 dBm 10 Hz 9600
NEO-6M (Generic) u-blox 6 $6 - $9 -160 dBm 5 Hz 9600
NEO-M9N u-blox M9 $28 - $35 -167 dBm 25 Hz 38400
ZOE-M8Q u-blox M8 $18 - $22 -162 dBm 10 Hz 9600

Bench Note: The VK-162 is the sweet spot for vehicle tracking and weather station builds. It includes an integrated LDO voltage regulator, meaning you can safely power it from the Pi's 5V pin or 3.3V pin, though its logic-level TX output is 3.3V, making it perfectly safe for the Pi 5's GPIO RX pin. Avoid the generic NEO-6M modules from unbranded marketplaces; over 60% of them use cloned silicon that fails to lock onto satellites indoors or under heavy tree cover.

Pin Mapping & Wiring Steps

The Raspberry Pi 5 routes the primary UART (ttyAMA0) to GPIO 14 and GPIO 15 by default, but you must ensure the Bluetooth module isn't hogging it. We will wire the module to the primary UART for the most stable timing, bypassing the mini-UART (ttyS0) which is tied to the core clock frequency and can drop NMEA characters under heavy CPU load.

Parts List

  • Raspberry Pi 5 (8GB recommended for concurrent logging)
  • VK-162 GPS Module with ceramic patch antenna
  • 4x Female-to-Female silicone jumper wires (26 AWG)
  • MicroSD card with Raspberry Pi OS Bookworm (64-bit) flashed

Pin Mapping Table

VK-162 Pin Pi 5 GPIO / Function Pi 5 Physical Pin Wire Color (Suggested)
VCC 3.3V Power Pin 1 Red
GND Ground Pin 6 Black
TXD GPIO 15 (RXD0) Pin 10 Green
RXD GPIO 14 (TXD0) Pin 8 Yellow

Configuration Steps

  1. Physical Wiring: Connect the wires exactly as mapped above. Safety Warning: Always power down the Pi and disconnect the USB-C PD cable before attaching jumper wires to the GPIO header to prevent accidental shorting of the 3.3V and 5V rails.
  2. Edit Firmware Config: Boot the Pi and open the terminal. On Bookworm, the config file moved. Type sudo nano /boot/firmware/config.txt.
  3. Add UART Overlays: Scroll to the bottom and add the following lines to enable the primary UART and disable the Bluetooth module's claim on it:
    # Enable primary UART on GPIO 14/15
    dtoverlay=disable-bt
    enable_uart=1
    
  4. Disable Serial Console: Run sudo raspi-config, navigate to Interface Options > Serial Port. Select No for "Would you like a login shell to be accessible over serial?" and Yes for "Would you like the serial port hardware to be enabled?".
  5. Reboot: Run sudo reboot. After rebooting, verify the port exists by running ls -l /dev/ttyAMA0. You should see a symlink pointing to the serial device.

Software Setup: gpsd and Python

We use gpsd as a middleware daemon. It handles the messy serial polling, parses the raw NMEA 0183 sentences, and exposes a clean JSON API over a local socket. For authoritative details on the daemon's architecture, refer to the official gpsd documentation.

  1. Install the daemon and Python bindings: sudo apt update && sudo apt install gpsd gpsd-clients python3-gps -y
  2. Configure gpsd to listen to our UART port: sudo nano /etc/default/gpsd. Modify the variables to match:
    START_DAEMON="true"
    GPSD_OPTIONS="-n"
    DEVICES="/dev/ttyAMA0"
    USBAUTO="false"
    
  3. Restart the service: sudo systemctl restart gpsd.
  4. Test the raw feed in terminal: cgps -s. If you see a 3D fix and satellite count, your hardware is working. (Note: The VK-162 may take 2-5 minutes to achieve a cold start fix indoors. Move near a window for initial testing).

Python Data Logging Script

Below is the complete, compilable Python script. It uses the gps3 library to connect to the local gpsd socket. It includes robust error handling for socket timeouts and daemon disconnects.

#!/usr/bin/env python3
"""
Target Board: Raspberry Pi 5 (8GB) running Raspberry Pi OS Bookworm
GPS Module: VK-162 (u-blox M8)
UART Pins: GPIO 14 (TXD) -> GPS RXD, GPIO 15 (RXD) -> GPS TXD
"""
import time
import sys
from gps3 import gps3

# Hardware Pin Definitions (for documentation/reference)
PIN_VCC = "3.3V (Physical Pin 1)"
PIN_GND = "GND (Physical Pin 6)"
PIN_TXD = "GPIO 14 / TXD0 (Physical Pin 8)"
PIN_RXD = "GPIO 15 / RXD0 (Physical Pin 10)"

GPSD_HOST = 'localhost'
GPSD_PORT = 2947

def main():
    try:
        gps_socket = gps3.GPSDSocket()
        data_stream = gps3.DataStream()
        gps_socket.connect(host=GPSD_HOST, port=GPSD_PORT)
        gps_socket.watch()
        print(f"Connected to gpsd on {GPSD_HOST}:{GPSD_PORT}")
    except Exception as e:
        print(f"FATAL: Could not connect to gpsd. Is the daemon running?\nError: {e}")
        sys.exit(1)

    try:
        while True:
            new_data = gps_socket.next()
            if new_data:
                data_stream.unpack(new_data)
                lat = data_stream.TPV['lat']
                lon = data_stream.TPV['lon']
                alt = data_stream.TPV['alt']
                
                # Check for valid fix (gpsd returns 'n/a' string when no fix)
                if isinstance(lat, float) and isinstance(lon, float):
                    print(f"FIX | Lat: {lat:.6f} | Lon: {lon:.6f} | Alt: {alt}m")
                else:
                    print("Searching for satellites... (No 3D Fix yet)")
            else:
                time.sleep(0.5) # Prevent CPU spinning on empty socket buffer
                
    except KeyboardInterrupt:
        print("\nLogging stopped by user.")
    except Exception as e:
        print(f"Runtime Error during GPS read: {e}")
    finally:
        gps_socket.close()

if __name__ == '__main__':
    main()

Debugging: Fixing UART and GPSD Errors

When working with raw UART on the Pi 5, you will inevitably hit serial contention or permission roadblocks. If your Python script fails or cgps shows no data, check these first three things:

  1. Verify the device node: Run ls -l /dev/ttyAMA0. If it says "No such file or directory", your config.txt overlays failed to apply, or you are looking for ttyS0 instead.
  2. Check daemon status: Run sudo systemctl status gpsd. Look for "Active: active (running)". If it's dead, it usually means the port is locked by another process.
  3. Sniff the raw wire: Run cat /dev/ttyAMA0. If you see scrolling NMEA sentences (e.g., $GPGGA,...), your hardware wiring is perfect and the issue is purely software/permissions. If it's dead air, check your TX/RX crossover.

Common Error Strings & Ranked Causes

Error 1: PermissionError: [Errno 13] Permission denied: '/dev/ttyAMA0'
Context: Occurs when running the Python script or cat without sudo.
Ranked Causes:
  1. User not in dialout group: Fix by running sudo usermod -a -G dialout $USER, then log out and log back in.
  2. gpsd has an exclusive lock: If gpsd is running, it claims the serial port. Your Python script should NOT read from /dev/ttyAMA0 directly; it must read from the gpsd socket (localhost:2947) as shown in the code above.
Error 2: gpsd:ERROR: SER: /dev/ttyAMA0 open error
Context: Occurs in the systemd journal when the gpsd daemon tries to start.
Ranked Causes:
  1. Serial console is still enabled: The Linux kernel is using the UART for boot logs. Re-run raspi-config and ensure the serial login shell is disabled.
  2. Bluetooth UART conflict: You forgot to add dtoverlay=disable-bt in /boot/firmware/config.txt. The Pi 5's Bluetooth chip defaults to the primary UART on some board revisions.
  3. Baud rate mismatch: While gpsd usually auto-detects, a hard lock can occur if the module was previously configured to 38400 baud but gpsd is probing at 9600. Add -s 9600 to GPSD_OPTIONS in /etc/default/gpsd to force the speed.

Extending or Simplifying the Build

Depending on your end goal, you may want to alter the complexity of this setup.

How to Simplify (The USB Route)

If you are building a quick prototype and don't want to deal with device tree overlays, UART pinouts, or config.txt, buy a BU-353-S4 USB GPS receiver (~$35). It contains a SiRF Star IV chip and an internal Prolific PL2303 USB-to-Serial bridge. You simply plug it into a USB-A port, and the Pi mounts it as /dev/ttyUSB0. You then point gpsd to /dev/ttyUSB0 and skip the GPIO wiring entirely. The trade-off is higher CPU interrupt overhead and occupying a valuable USB 3.0 port on the Pi 5.

How to Extend (Headless Logging & Display)

For a standalone vehicle tracker or marine navigation node:

  • Add I2C OLED Output: Wire a 0.96" SSD1306 OLED display to the Pi's I2C pins (GPIO 2/3). Modify the Python script to push the lat, lon, and speed variables to the display using the Adafruit_SSD1306 library, allowing you to verify fixes without an SSH session or HDMI monitor.
  • SQLite Geofence Logging: Instead of printing to the console, import Python's sqlite3 library. Create a table with timestamp, latitude, longitude, altitude. Add a logic block that calculates the Haversine distance between the current coordinate and a "home base" coordinate, triggering a webhook via the requests library if the Pi leaves a 500-meter radius.
  • Antenna Upgrades: The VK-162's included ceramic patch antenna is adequate for open sky. If mounting the Pi inside a metal enclosure or deep inside a vehicle dashboard, swap the SMA connector for an active, high-gain external antenna (like the u-blox ANN-MB series) and route it to the windshield for unobstructed sky view.