For 90% of embedded tracking and timing projects, the best GPS module with Raspberry Pi is the u-blox NEO-M8N connected via the Pi's hardware UART (GPIO 14/15). While cheap USB GPS dongles exist, they consume a full USB bus, draw excess current, and introduce polling latency. Hardwiring a genuine NEO-M8N breakout to the Pi's serial pins yields a robust, low-power (under 45mA) solution capable of 10Hz updates and locking onto 30+ satellites simultaneously. Below is the exact decision path, wiring diagram, and Python code to get NMEA sentences parsing reliably on Raspberry Pi OS Bookworm.

Decision Tree: Choosing the Right GPS Module with Raspberry Pi

Don't just buy the first module on Amazon. Use this decision matrix to select the exact hardware for your payload constraints and accuracy requirements.

If your project requires... Then choose this module... Why / Caveats
Budget under $12, basic outdoor logging NEO-6M (Generic) Warning: Over 80% of cheap NEO-6Ms are counterfeit clones with terrible sensitivity and no backup battery. Avoid unless prototyping.
Reliable outdoor tracking, 10Hz updates, standard hobbyist use u-blox NEO-M8N (Default Pick) Best balance of price ($35-$45), genuine silicon availability, and 3V3/5V tolerant breakouts. Excellent multipath mitigation.
Sub-meter accuracy, drone RTK, or precision agriculture ZED-F9P (RTK) Centimeter-level accuracy, but requires an NTRIP caster, base station corrections, and costs $200+. Overkill for basic tracking.
Ultra-low power, battery-operated asset tracker PA1010D (MTK3339) Great sleep modes, but lower update rates and fewer channels than the M8N. Better suited for Pi Pico than a full Pi.
Bench Tip: Always buy the NEO-M8N from a reputable distributor (like SparkFun, Adafruit, or direct from u-blox partners). Generic "NEO-M8N" boards on AliExpress frequently use recycled or rejected die that fail to achieve a 3D fix indoors or under tree canopy.

Hardware Spec Sheet and Pin Mapping

This build targets the Raspberry Pi 4 Model B (4GB) or Raspberry Pi 5 running Raspberry Pi OS (Bookworm, 64-bit). The code and configuration steps apply to both, though the Pi 5's PCIe and USB3 architecture makes keeping the hardware UART free for the GPS even more important to avoid bus contention.

Bill of Materials

  • Compute: Raspberry Pi 4B (4GB) or Pi 5 (~$55 - $80)
  • GPS: u-blox NEO-M8N Breakout Board with genuine chip (~$40)
  • Antenna: Active GPS Antenna, SMA connector, 28dB gain, 5m cable (~$15)
  • Wiring: 4x Silicone Female-to-Female jumper wires (20cm) (~$5)

UART Pin Mapping

The Raspberry Pi exposes its primary UART on the 40-pin GPIO header. We are using the hardware PL011 UART (mapped to /dev/ttyAMA0 after disabling Bluetooth), not the mini-UART, to ensure stable baud rates under CPU load.

Pi GPIO Pin (Physical) Pi Function NEO-M8N Breakout Pin Notes
Pin 1 3V3 Power VCC Use 3V3 if your breakout has no regulator. If it has an AMS1117-3.3, use 5V (Pin 2).
Pin 6 Ground GND Common ground is critical for stable serial logic levels.
Pin 8 (GPIO 14) TXD (Transmit) RX Pi TX goes to GPS RX.
Pin 10 (GPIO 15) RXD (Receive) TX Pi RX goes to GPS TX.

Wiring and OS Configuration (Bookworm)

By default, Raspberry Pi OS routes the system serial console to the UART and assigns the Bluetooth module to the high-performance PL011 UART. We need to reverse this: disable Bluetooth, free up the PL011 UART for the GPS, and kill the serial console login prompt so it doesn't corrupt our NMEA stream.

Difficulty: Intermediate | Time: 15 minutes
  1. Disable the Serial Console: Open terminal and 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?".
  2. Disable Bluetooth: Edit the boot config file. Note that in Bookworm, this is located in /boot/firmware/, not /boot/.
    sudo nano /boot/firmware/config.txt
    Add this line to the very bottom: dtoverlay=disable-bt
  3. Disable the HCI UART service: This prevents the OS from trying to initialize Bluetooth on the serial port.
    sudo systemctl disable hciuart
  4. Reboot the Pi: sudo reboot
  5. Verify the Port: After rebooting, check your serial devices.
    ls -l /dev/serial*
    You should see /dev/serial0 symlinked to /dev/ttyAMA0. This is your GPS port.

Complete Python Tracking Script with Error Handling

This script uses pyserial to read the raw byte stream and pynmea2 to parse the NMEA 0183 sentences. We specifically filter for GGA (Global Positioning System Fix Data) sentences, which contain latitude, longitude, and altitude. Install the dependencies first: pip install pyserial pynmea2.

import serial
import pynmea2
import time
import sys

# --- PIN & PORT DEFINITIONS ---
# On Pi 4/5 with BT disabled, hardware UART is ttyAMA0
UART_PORT = '/dev/ttyAMA0'
BAUD_RATE = 9600  # Default for u-blox NEO-M8N
TIMEOUT_SEC = 1

def init_serial():
    try:
        ser = serial.Serial(
            port=UART_PORT,
            baudrate=BAUD_RATE,
            timeout=TIMEOUT_SEC,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            bytesize=serial.EIGHTBITS
        )
        return ser
    except serial.SerialException as e:
        print(f"[FATAL] Hardware error: {e}")
        sys.exit(1)
    except PermissionError as e:
        print(f"[FATAL] OS Permission error: {e}")
        print("Fix: Run 'sudo usermod -a -G dialout $USER' and reboot.")
        sys.exit(1)

def parse_gps_stream(ser):
    print(f"Listening on {UART_PORT} at {BAUD_RATE} baud...")
    while True:
        try:
            # Read a line from the serial buffer
            raw_line = ser.readline()
            
            # Filter out empty reads and non-NMEA garbage
            if not raw_line or not raw_line.startswith(b'$'):
                continue
                
            # Decode bytes to string and strip trailing whitespace/newlines
            nmea_sentence = raw_line.decode('ascii', errors='replace').strip()
            
            # Parse the sentence
            msg = pynmea2.parse(nmea_sentence)
            
            # We only care about GGA sentences for standard fix data
            if isinstance(msg, pynmea2.types.talker.GGA):
                print(f"[FIX] Lat: {msg.latitude:.6f} | Lon: {msg.longitude:.6f} | Alt: {msg.altitude:.1f}m | Sats: {msg.num_sats}")
                
        except pynmea2.ParseError as e:
            # NMEA checksum failure or malformed string (common during buffer misalignment)
            pass
        except UnicodeDecodeError:
            # Ignore binary garbage if the module switches to UBX protocol
            pass
        except KeyboardInterrupt:
            print("\nStopping GPS stream.")
            break
        except serial.SerialException as e:
            print(f"[ERROR] Serial connection lost: {e}")
            break

if __name__ == '__main__':
    serial_conn = init_serial()
    parse_gps_stream(serial_conn)
    serial_conn.close()

Debugging: The First Three Things to Check When It Fails

UART debugging on the Pi is notoriously frustrating because the OS silently swallows errors or routes them to the wrong device tree overlay. If your script outputs nothing or crashes, check these three exact failure modes in order.

1. The Permission Denied Crash

Exact Error String: PermissionError: [Errno 13] Permission denied: '/dev/ttyAMA0'

  • Cause: Your current user (usually pi or your custom username) does not have read/write access to the serial TTY device.
  • Fix: Add your user to the dialout group. Run sudo usermod -a -G dialout $USER, then completely log out and log back in (or reboot) for the group change to take effect.

2. The Missing Port Exception

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

  • Cause: The hardware UART is not enabled, or Bluetooth is still hogging the PL011 controller, pushing the GPS to the mini-UART (/dev/ttyS0).
  • Fix: Verify your /boot/firmware/config.txt has dtoverlay=disable-bt. Run ls -l /dev/serial*. If serial0 points to ttyS0, your overlay failed to load. Check for typos in config.txt and ensure you ran sudo systemctl disable hciuart.

3. The NMEA Parse Error Loop

Exact Error String: pynmea2.ParseError: could not parse... (Firing continuously in your console)

  • Cause: Buffer misalignment. The Pi's serial buffer read a partial sentence (e.g., starting in the middle of a GSV sentence) and passed it to the parser, or the GPS module is outputting a proprietary UBX binary sentence that pynmea2 doesn't understand.
  • Fix: The provided code handles this by checking raw_line.startswith(b'$') and catching the exception. If it persists, use the u-blox u-center software on a Windows PC via a USB-to-Serial adapter to reconfigure the NEO-M8N to output only NMEA standard sentences (disable UBX, GLONASS, or BeiDou if you only need basic GPS).

Extending and Simplifying the Build

Once you have a stable 3D fix, you can adapt this hardware stack to fit different project constraints.

How to Simplify (The I2C Route)

If you are out of UART ports (e.g., you need the hardware UART for a LoRaWAN concentrator), many premium NEO-M8N breakouts (like the SparkFun GPS-15193) expose an I2C interface (DDC).

  • Wiring: Connect Pi GPIO 2 (SDA) to GPS SDA, and GPIO 3 (SCL) to GPS SCL.
  • Code Change: Use the smbus2 library to read the I2C registers directly. Note that I2C is significantly slower; you will need to poll the DDC data stream register (0xFF) in a tight loop to prevent buffer overruns at 9600 baud equivalent data rates.

How to Extend (Logging and RTK)

  • Add Local Logging: Pipe the parsed GGA data into a local SQLite database or an InfluxDB time-series instance. Use the datetime module to timestamp the data using the Pi's system clock, but trust the GPS time for the actual fix timestamp to account for serial latency.
  • Upgrade to RTK: If you swap the NEO-M8N for a ZED-F9P module, the Python code remains 90% identical. You will simply need to add a second serial stream (via USB) to inject RTCM3 correction data from an NTRIP caster into the F9P's RX pin to achieve centimeter-level accuracy.

By hardwiring the UART and stripping away the OS-level serial console interference, your Raspberry Pi transforms from a fragile Linux box into a dedicated, high-reliability GNSS receiver capable of running headless in the field for months on end.