The most reliable and cost-effective GPS module for a Raspberry Pi UART setup is the u-blox NEO-6M breakout board (typically the GY-GPS6MV2 variant, ~$12). You will wire its TX pin to the Pi's GPIO 15 (RXD) and its RX pin to GPIO 14 (TXD), communicating at a default baud rate of 9600. This guide covers the exact hardware pinout, Raspberry Pi OS Bookworm UART configuration, and a complete Python script to parse NMEA sentences.

Board Variant Note: This guide targets the Raspberry Pi 4 Model B running Raspberry Pi OS (Bookworm or later). If you are using a Raspberry Pi 5, the UART mapping changes slightly; see the FAQ section for the Pi 5 dtoverlay adjustment.

Project Overview & Parts List

Before writing code, you need the right hardware. Cheap GPS modules often ship with passive antennas that fail indoors. Ensure your kit includes an active antenna (one with a small ceramic patch and an LNA circuit).

Required Hardware and Estimated 2026 Pricing
Component Exact Variant / Spec Est. Cost
Microcontroller Raspberry Pi 4 Model B (4GB RAM) $55.00
GPS Module u-blox NEO-6M (GY-GPS6MV2 breakout with LDO) $12.00
Antenna Active GPS Antenna (U.FL to SMA, 28dB gain) $8.00
Wiring Female-to-Female Jumper Wires (20cm) $5.00
Storage 32GB MicroSD (Class 10, A1 rating) $8.00

Hardware Wiring & UART Configuration

The NEO-6M breakout board communicates via UART. The Raspberry Pi 4 exposes its primary UART on GPIO 14 (TXD) and GPIO 15 (RXD).

Pin Mapping Table

NEO-6M Pin Raspberry Pi 4 Pin BCM GPIO Notes
VCC Pin 2 (5V) N/A Use 5V. The GY-GPS6MV2 has an onboard AMS1117-3.3 LDO to regulate this for the NEO-6M chip.
GND Pin 6 (Ground) N/A Common ground is mandatory for serial communication.
TXD Pin 10 (RXD) GPIO 15 Cross-wiring: Module TX goes to Pi RX.
RXD Pin 8 (TXD) GPIO 14 Cross-wiring: Module RX goes to Pi TX.

Enabling the Hardware UART

By default, the Raspberry Pi routes the primary UART to the Bluetooth module and uses the mini-UART for the GPIO pins. We need to reverse this for stable GPS timing.

  1. Open the terminal and run sudo raspi-config.
  2. Navigate to Interface Options > Serial Port.
  3. When asked "Would you like a login shell to be accessible over serial?", select No.
  4. When asked "Would you like the serial port hardware to be enabled?", select Yes.
  5. Reboot the Pi.

Next, verify the configuration in the boot config file. On Raspberry Pi OS Bookworm, the path has moved from /boot/config.txt to /boot/firmware/config.txt. Open it with sudo nano /boot/firmware/config.txt and ensure this line is at the very bottom:

enable_uart=1

According to the official Raspberry Pi UART documentation, setting enable_uart=1 automatically maps the PL011 hardware UART to the GPIO pins and assigns the mini-UART to Bluetooth.

Python Code for NMEA Parsing

GPS modules output raw NMEA 0183 sentences (like $GPGGA and $GPRMC). Parsing these manually with string splits is brittle. We will use the pynmea2 library alongside pyserial. Install them via pip:

pip install pyserial pynmea2

The following script targets the Pi 4's /dev/serial0 symlink. It includes robust error handling for serial dropouts and malformed NMEA checksums.

import serial
import pynmea2
import time
import sys

# Pin/Port Definitions for Raspberry Pi 4
SERIAL_PORT = '/dev/serial0'
BAUD_RATE = 9600
TIMEOUT = 1.5

def main():
    try:
        # Initialize serial connection
        ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=TIMEOUT)
        print(f"Listening for GPS data on {SERIAL_PORT}...")
    except serial.SerialException as e:
        print(f"FATAL: Could not open serial port. {e}")
        sys.exit(1)

    try:
        while True:
            try:
                # Read a line from the serial buffer and decode
                raw_line = ser.readline().decode('ascii', errors='replace').strip()
                
                # Only process lines that start with '$' (NMEA standard)
                if raw_line.startswith('$'):
                    msg = pynmea2.parse(raw_line)
                    
                    # Extract data specifically from the Recommended Minimum sentence (RMC)
                    if isinstance(msg, pynmea2.types.talker.RMC):
                        if msg.status == 'A':  # 'A' = Active/Fix, 'V' = Void/No Fix
                            print(f"[FIX] Lat: {msg.latitude} {msg.lat_dir}, "
                                  f"Lon: {msg.longitude} {msg.lon_dir}, "
                                  f"Speed: {msg.spd_over_grnd} knots")
                        else:
                            print("[SEARCHING] Waiting for satellite lock...")
                            
            except pynmea2.ParseError as e:
                # Handles corrupted bytes or failed checksums
                print(f"Parse error: {e}")
                continue
                
    except KeyboardInterrupt:
        print("\nStopping GPS reader.")
    finally:
        if 'ser' in locals() and ser.is_open:
            ser.close()

if __name__ == '__main__':
    main()
Safety & Hardware Warning: Never plug a raw NEO-6M chip directly into the Pi's 5V pin without a breakout board. The raw chip operates at 3.3V and will be destroyed by 5V. The GY-GPS6MV2 breakout board mentioned in the parts list includes the necessary logic-level regulation.

Debugging Serial Port Errors

When running the script, the most common failure point is the OS blocking access to the UART. If you encounter this exact error string:

serial.serialutil.SerialException: [Errno 2] could not open port /dev/serial0: [Errno 2] No such file or directory: '/dev/serial0'

Do not immediately assume your hardware is dead. This is almost always a software routing issue. Here are the first three things to check, ranked by likelihood:

  1. The enable_uart=1 flag is missing or in the wrong file. On older OS versions (Bullseye and earlier), this goes in /boot/config.txt. On Bookworm and newer, it must go in /boot/firmware/config.txt. If it's in the wrong directory, the kernel ignores it.
  2. The serial console is still active. If you skipped the raspi-config step, the Linux kernel is using /dev/serial0 to output boot logs. Check /boot/firmware/cmdline.txt and remove the phrase console=serial0,115200 if it exists. (Be careful not to delete the rest of the single-line file).
  3. You are looking for /dev/ttyAMA0 instead of /dev/serial0. The /dev/serial0 symlink is dynamically mapped to whichever UART is assigned to the GPIO pins. Hardcoding /dev/ttyAMA0 in your Python script will fail if the OS swaps the Bluetooth and hardware UARTs. Always use /dev/serial0.

If the port opens but you get no output (or only the "[SEARCHING]" message), your module is experiencing a "cold start." The NEO-6M needs a clear view of the sky and up to 15 minutes to download the almanac data on its very first boot. Move the active antenna near a window or outdoors to establish the initial lock.

Extending and Simplifying the Build

How to Simplify: Use the gpsd Daemon

If you don't want to write custom Python serial parsers, install the gpsd service (sudo apt install gpsd gpsd-clients). It runs in the background, handles the serial buffering, and exposes the GPS data over a local TCP socket. You can then query it using the gps Python library, which abstracts away all NMEA parsing.

How to Extend: Add an I2C OLED Display

To make this a standalone tracker, add a 0.96" SSD1306 OLED display via I2C. Wire the OLED's SDA to GPIO 2 (Pin 3) and SCL to GPIO 3 (Pin 5). Use the adafruit-circuitpython-ssd1306 library to render the latitude, longitude, and satellite count directly to the screen without needing an SSH terminal.

Frequently Asked Questions

Why is my GPS module Raspberry Pi setup not getting a fix indoors?

GPS signals operate at 1.575 GHz and are easily blocked by roofing materials, concrete, and energy-efficient window coatings. The NEO-6M requires a direct line of sight to at least 4 satellites to calculate a 3D fix. If you must test indoors, place the active antenna directly against a single-pane glass window, but expect a cold-start acquisition time of 10 to 15 minutes. For indoor testing, consider using a GPS simulator or moving outside for the initial almanac download.

Can I use a USB GPS dongle instead of wiring UART GPIO pins?

Yes. USB GPS receivers (like the VK-172 based on the u-blox 7 chipset) are plug-and-play. When plugged in, they mount as /dev/ttyACM0 or /dev/ttyUSB0. Simply change the SERIAL_PORT variable in the Python code above to match the USB device path. This bypasses all UART configuration headaches, though it consumes a USB port and slightly more power.

Does the NEO-6M code work with the Raspberry Pi 5?

The Python code remains identical, but the Raspberry Pi 5 handles UART mapping differently due to its new PCIe and southbridge architecture. To enable the GPIO UART on a Pi 5, you must add dtoverlay=uart0 (or uart1 depending on your specific board revision) to your /boot/firmware/config.txt. Consult the gpsd NMEA reference and Pi 5 specific hardware notes to verify your exact overlay requirement.

How do I log the GPS coordinates to a CSV file?

Import Python's built-in csv module. Inside the if msg.status == 'A': block of the script, open a file in append mode (with open('gps_log.csv', 'a') as f:) and use a csv.writer object to write a row containing msg.timestamp, msg.latitude, and msg.longitude. Ensure you include a header row check so the column names are only written once.