If you are adding location tracking to an embedded project, the default pick for a reliable, low-cost gps module for raspberry pi is the GY-GPS6MV2 (u-blox NEO-6M clone). It costs around $12, operates at 3.3V logic (safe for Pi GPIO), and outputs standard NMEA-0183 sentences over UART at 9600 baud. While premium RTK modules exist for surveying, the NEO-6M hits the sweet spot for vehicle tracking, weather stations, and marine logging where 2.5-meter accuracy is sufficient.

This guide walks through the exact hardware wiring, the Raspberry Pi OS UART configuration traps that brick 90% of first-time builds, and a production-ready Python script with robust error handling.

The Verdict: Which GPS Module for Raspberry Pi Should You Buy?

Do not buy a module until you define your accuracy requirement and interface preference. Use this decision matrix to select the exact part number for your bench.

GPS Module Decision Tree for Raspberry Pi
Use Case Module Pick Chipset / Interface Price Range Verdict
Basic tracking, logging, geofencing GY-GPS6MV2 u-blox NEO-6M / UART $10 - $15 DEFAULT PICK. Best value for 95% of hobbyist and IoT projects.
Reliable fixes, indoor lock, datalogging Adafruit Ultimate GPS V3 MTK3339 / UART $35 - $45 Choose when you need a built-in coin cell for battery-backed RTC and faster cold starts.
Sub-meter accuracy, robotics, drones SparkFun GPS-RTK2 u-blox ZED-F9P / I2C+UART $200 - $240 Choose only if you have an RTK base station and need centimeter-level precision.
Quick prototyping, avoiding GPIO config VK-162 USB Dongle u-blox NEO-M8N / USB $15 - $20 Choose to bypass UART entirely. Plugs into USB, mounts as /dev/ttyACM0.

Hardware Spec Sheet and GPIO Pin Mapping

This build targets the Raspberry Pi 4 Model B (4GB) and the Raspberry Pi 5. Both boards use the same primary UART mapping on the 40-pin header, though the underlying OS file paths differ slightly (addressed in the next section).

Difficulty Rating: Intermediate (Requires OS-level config file edits and serial terminal management).
Time to Complete: 45 minutes.

Parts List

  • Microcontroller: Raspberry Pi 4 Model B (4GB) or Raspberry Pi 5
  • GPS Module: GY-GPS6MV2 (u-blox NEO-6M breakout board)
  • Antenna: Active GPS ceramic patch antenna with u.FL to SMA pigtail (usually included with the module)
  • Wiring: 4x Female-to-Female silicone jumper wires (26 AWG)

Pin Mapping Table

Critical Rule: UART requires crossed lines. The Pi's Transmit (TX) must connect to the GPS's Receive (RX), and vice versa.

Raspberry Pi 40-Pin Header GPIO / Function GY-GPS6MV2 Pin Wire Color (Suggested)
Pin 1 3.3V Power VCC Red
Pin 6 Ground GND Black
Pin 8 GPIO 14 (TXD) RX Yellow
Pin 10 GPIO 15 (RXD) TX Green
⚠️ Voltage Warning: The NEO-6M breakout operates at 3.3V. Never connect the GPS VCC pin to the Pi's 5V (Pin 2) rail. While some clone boards have onboard LDO regulators that tolerate 5V, feeding 3.3V directly bypasses the regulator, reducing heat and preventing brownouts during satellite acquisition spikes (which can pull 45mA).

OS Configuration: Freeing the UART from Bluetooth

The most common reason a gps module for raspberry pi fails is that the Pi's primary hardware UART (ttyAMA0) is hardcoded to route to the onboard Bluetooth chip, leaving the GPIO pins attached to the inferior 'mini UART' (ttyS0), which lacks a stable baud rate clock. We must force the OS to route the primary PL011 UART back to the GPIO header.

  1. Disable the Serial Console: Run sudo raspi-config, navigate to Interface Options > Serial Port. Select No to 'Would you like a login shell to be accessible over serial?', and Yes to 'Would you like the serial port hardware to be enabled?'.
  2. Edit the Boot Config: Open the configuration file.
    • For Pi 4 (older OS): sudo nano /boot/config.txt
    • For Pi 4 (Bookworm) / Pi 5: sudo nano /boot/firmware/config.txt
  3. Add the Overlays: Add these exact lines to the bottom of the file:
    enable_uart=1
    dtoverlay=disable-bt
  4. Disable the Bluetooth Service: Prevent the hciuart service from trying to initialize the now-disabled Bluetooth chip.
    sudo systemctl disable hciuart
  5. Reboot: sudo reboot. Your GPIO UART is now mapped to /dev/ttyAMA0.

Python NMEA Parsing Code (Target: Pi 4B / Pi 5)

This script uses the pynmea2 library to parse raw NMEA sentences. Install dependencies via terminal before running: pip3 install pyserial pynmea2.

The code includes explicit pin/port definitions and handles the two most common failure modes: serial port lockouts and malformed NMEA checksums.

import serial
import pynmea2
import time
import sys

# --- HARDWARE DEFINITIONS ---
# Target Board: Raspberry Pi 4B / Pi 5
# Port: /dev/ttyAMA0 (Primary PL011 UART, mapped via dtoverlay=disable-bt)
# Baud: 9600 (Default for u-blox NEO-6M)
GPS_PORT = '/dev/ttyAMA0'
BAUD_RATE = 9600
TIMEOUT_SEC = 5

def init_serial_port():
    try:
        port = serial.Serial(GPS_PORT, BAUD_RATE, timeout=TIMEOUT_SEC)
        print(f'Successfully opened {GPS_PORT} at {BAUD_RATE} baud.')
        return port
    except serial.SerialException as e:
        print(f'FATAL: Could not open serial port. Is Bluetooth disabled in config.txt?')
        print(f'Error: {e}')
        sys.exit(1)

def parse_gps_stream(port):
    while True:
        try:
            # Read a line from the serial buffer
            raw_line = port.readline().decode('ascii', errors='replace').strip()
            
            # Ignore empty reads (timeout) or non-NMEA data
            if not raw_line or not raw_line.startswith('$'):
                continue

            # Parse the NMEA sentence
            msg = pynmea2.parse(raw_line)
            
            # We only care about GGA sentences (Global Positioning System Fix Data)
            if isinstance(msg, pynmea2.types.talker.GGA):
                if msg.gps_qual == 0:
                    print('Status: Searching for satellites...')
                else:
                    print(f'Fix Quality: {msg.gps_qual} | Lat: {msg.latitude:.6f} | Lon: {msg.longitude:.6f} | Alt: {msg.altitude}m | Sats: {msg.num_sats}')
                    
        except pynmea2.ParseError as e:
            # Triggered when NMEA checksum fails (common on noisy breadboard wires)
            print(f'Parse Error (discarding sentence): {e}')
            continue
            
        except serial.SerialException as e:
            print(f'Serial stream interrupted: {e}')
            break
        
        except KeyboardInterrupt:
            print('\nTracking stopped by user.')
            break

if __name__ == '__main__':
    ser = init_serial_port()
    try:
        parse_gps_stream(ser)
    finally:
        if ser.is_open:
            ser.close()
            print('Serial port closed cleanly.')

Debugging: "device reports readiness to read but returned no data"

If your script crashes immediately upon execution, you will likely see this exact error string:

serial.serialutil.SerialException: device reports readiness to read but returned no data (device disconnected or multiple access on port?)

This is the hallmark of a port contention issue. The OS is holding the UART open in the background. Here are the first three things to check, ranked by likelihood:

  1. The serial-getty service is still active. Even if you disabled the login shell in raspi-config, systemd might still be running a getty process on the port.
    Fix: Run sudo systemctl stop serial-getty@ttyAMA0.service and sudo systemctl disable serial-getty@ttyAMA0.service. (Replace ttyAMA0 with ttyS0 if you didn't disable Bluetooth).
  2. Bluetooth was not successfully disabled. If dtoverlay=disable-bt was added to the wrong config.txt file (e.g., you edited /boot/config.txt on a Pi 5 running Bookworm, which ignores it in favor of /boot/firmware/config.txt), the OS routes the port to the BT chip, causing garbage data and lockups.
    Fix: Verify your edit path. Run dmesg | grep tty to confirm ttyAMA0 is mapped to the PL011 hardware, not the mini UART.
  3. TX and RX are wired straight-through instead of crossed. If Pi TX goes to GPS TX, the Pi's transmit line pulls the GPS transmit line high, causing a hardware collision that results in the kernel dropping the port state.
    Fix: Swap the yellow and green jumper wires on the GPIO header (Pin 8 and Pin 10).

Extending or Simplifying Your Build

Depending on your project timeline and end-goal, you may need to pivot from the default NEO-6M UART setup.

How to Simplify (The USB Bypass)

If you are building a quick proof-of-concept and do not want to deal with config.txt overlays, buy a VK-162 USB GPS module. It contains a u-blox chipset but interfaces via USB.

  • Plug it into the Pi.
  • It mounts automatically as /dev/ttyACM0.
  • Change GPS_PORT = '/dev/ttyAMA0' to GPS_PORT = '/dev/ttyACM0' in the Python script above.
  • No UART configuration or GPIO wiring required.

How to Extend (MQTT and Home Assistant)

To turn this standalone logger into an IoT node, integrate the paho-mqtt library. Inside the isinstance(msg, pynmea2.types.talker.GGA) block, format the coordinates as a JSON payload and publish to an MQTT broker:

import json
import paho.mqtt.client as mqtt

client = mqtt.Client('Pi_GPS_Node')
client.connect('192.168.1.100', 1883, 60)

# Inside the GGA parsing block:
payload = json.dumps({'lat': msg.latitude, 'lon': msg.longitude, 'alt': msg.altitude})
client.publish('homeassistant/sensor/pi_gps/state', payload)

This allows Home Assistant to ingest the GPS data via the MQTT integration, enabling automations like turning on your driveway lights when your vehicle crosses a geofence boundary.

For deeper hardware specifications on the u-blox 6 positioning engine, refer to the official u-blox NEO-6M datasheet. For comprehensive details on Raspberry Pi peripheral routing, consult the Raspberry Pi UART configuration documentation.