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.
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
- 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.
- Edit Firmware Config: Boot the Pi and open the terminal. On Bookworm, the config file moved. Type
sudo nano /boot/firmware/config.txt. - 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
- 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?". - Reboot: Run
sudo reboot. After rebooting, verify the port exists by runningls -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.
- Install the daemon and Python bindings:
sudo apt update && sudo apt install gpsd gpsd-clients python3-gps -y - 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"
- Restart the service:
sudo systemctl restart gpsd. - 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:
- Verify the device node: Run
ls -l /dev/ttyAMA0. If it says "No such file or directory", yourconfig.txtoverlays failed to apply, or you are looking forttyS0instead. - 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. - 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
PermissionError: [Errno 13] Permission denied: '/dev/ttyAMA0'Context: Occurs when running the Python script or
cat without sudo.Ranked Causes:
- User not in dialout group: Fix by running
sudo usermod -a -G dialout $USER, then log out and log back in. - gpsd has an exclusive lock: If
gpsdis running, it claims the serial port. Your Python script should NOT read from/dev/ttyAMA0directly; it must read from thegpsdsocket (localhost:2947) as shown in the code above.
gpsd:ERROR: SER: /dev/ttyAMA0 open errorContext: Occurs in the systemd journal when the gpsd daemon tries to start.
Ranked Causes:
- Serial console is still enabled: The Linux kernel is using the UART for boot logs. Re-run
raspi-configand ensure the serial login shell is disabled. - Bluetooth UART conflict: You forgot to add
dtoverlay=disable-btin/boot/firmware/config.txt. The Pi 5's Bluetooth chip defaults to the primary UART on some board revisions. - Baud rate mismatch: While
gpsdusually auto-detects, a hard lock can occur if the module was previously configured to 38400 baud butgpsdis probing at 9600. Add-s 9600toGPSD_OPTIONSin/etc/default/gpsdto 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, andspeedvariables to the display using theAdafruit_SSD1306library, allowing you to verify fixes without an SSH session or HDMI monitor. - SQLite Geofence Logging: Instead of printing to the console, import Python's
sqlite3library. Create a table withtimestamp, 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 therequestslibrary 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.






