Getting a GPS on Raspberry Pi working reliably requires bypassing the default serial console, correctly mapping the 3.3V UART pins, and handling NMEA-0183 sentence parsing. For this build, we are targeting the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS Bookworm, paired with the ubiquitous u-blox NEO-6M module.
The direct answer: Wire the NEO-6M TX pin to Pi GPIO 15 (RXD), disable the serial login shell in raspi-config, enable the PL011 UART via config.txt, and parse the 9600-baud NMEA stream using Python’s pynmea2 library.
• Difficulty: Intermediate (Requires Linux CLI and basic soldering/pin mapping)
• Time to Complete: 45 minutes
• Target Board: Raspberry Pi 5 (8GB) with RP1 Southbridge
• Target OS: Raspberry Pi OS (Bookworm 64-bit)
Hardware BOM and GPS Module Comparison
Before wiring, it is worth verifying your exact GPS module. The NEO-6M is the most common hobbyist board, but if you are building a high-altitude balloon or a precision rover in 2026, you may want to upgrade. Below is a data-dense comparison of the most common breakout boards you will encounter.
| Module Variant | GNSS Constellations | Cold Start Time | Typical 2026 Price | Logic Level |
|---|---|---|---|---|
| u-blox NEO-6M | GPS (L1 C/A only) | ~27 seconds | $12 - $15 | 3.3V (w/ LDO) |
| u-blox NEO-M9N | GPS, GLONASS, Galileo, BeiDou | ~24 seconds | $45 - $55 | 3.3V / 1.8V |
| Quectel L76K | GPS, GLONASS, Galileo, QZSS | ~26 seconds | $18 - $22 | 3.3V |
| u-blox MAX-M10S | GPS, GLONASS, Galileo, BeiDou | ~25 seconds | $25 - $30 | 3.3V / 1.8V |
Exact Pin Mapping: Pi 5 to NEO-6M
The Raspberry Pi 5 routes its primary UART through the RP1 southbridge chip. The PL011 UART is exposed on the standard 40-pin header. Because the NEO-6M breakout board includes an onboard 3.3V LDO regulator and logic-level shifting, you can wire it directly to the Pi 5 without a logic level converter.
| Pi 5 Physical Pin | BCM GPIO | NEO-6M Pin | Wire Color (Std) | Notes |
|---|---|---|---|---|
| Pin 2 | 5V Power | VCC | Red | Powers the onboard 3.3V LDO |
| Pin 6 | GND | GND | Black | Common ground reference |
| Pin 8 | GPIO 14 (TXD) | RXD | Yellow | Pi transmits to GPS (optional for read-only) |
| Pin 10 | GPIO 15 (RXD) | TXD | Green | GPS transmits NMEA data to Pi |
Step-by-Step Wiring and UART Configuration
By default, the Raspberry Pi routes the system console (login shell) to the serial UART. If you do not disable this, the Pi will flood the GPS module with boot text, and your Python script will be blocked from accessing the port. Follow the official Raspberry Pi UART configuration guidelines to free up the hardware port.
- Wire the hardware: Connect VCC to Pin 2 (5V), GND to Pin 6, GPS TXD to Pi Pin 10 (GPIO 15/RXD). You can skip Pi TXD to GPS RXD unless you plan to send UBX configuration commands to the module.
- Open terminal: Run
sudo raspi-config. - Navigate: Go to Interface Options → Serial Port.
- Disable Console: When asked "Would you like a login shell to be accessible over serial?", select No.
- Enable Hardware: When asked "Would you like the serial port hardware to be enabled?", select Yes.
- Force PL011 UART: Open the boot config file:
sudo nano /boot/firmware/config.txt(Note: on Bookworm, the path is/boot/firmware/, not/boot/). Add the following line at the very bottom:enable_uart=1
- Reboot: Run
sudo reboot. The UART is now mapped cleanly to/dev/ttyAMA0(also symlinked as/dev/serial0).
Python NMEA Parsing Code
We will use pyserial to read the raw byte stream and pynmea2 to parse the NMEA-0183 sentences. Install the dependencies first: pip3 install pyserial pynmea2. For deeper protocol details, refer to the pynmea2 GitHub repository.
The code below targets the Pi 5 PL011 UART, includes explicit pin/port definitions, and features robust error handling for serial drops and malformed checksums.
import serial
import pynmea2
import sys
import time
# --- Pin & Port Definitions ---
# Pi 5 PL011 UART mapped to GPIO 14/15 via enable_uart=1
UART_PORT = '/dev/ttyAMA0'
BAUD_RATE = 9600 # NEO-6M default hardware baud rate
TIMEOUT = 1.5 # Seconds to wait for a serial byte
def parse_gps_stream():
"""Reads and parses NMEA sentences from the UART stream."""
try:
ser = serial.Serial(UART_PORT, BAUD_RATE, timeout=TIMEOUT)
print(f"[INFO] Listening on {UART_PORT} at {BAUD_RATE} baud...")
while True:
# Read raw bytes, decode to ASCII, ignore non-ASCII garbage
raw_line = ser.readline().decode('ascii', errors='replace').strip()
# NMEA sentences always start with '$'
if raw_line.startswith('$'):
try:
msg = pynmea2.parse(raw_line)
# GGA contains essential fix data (Lat/Lon/Alt/Satellites)
if isinstance(msg, pynmea2.types.talker.GGA):
if msg.gps_qual > 0:
print(f"[FIX] Quality: {msg.gps_qual} | "
f"Lat: {msg.latitude:0.6f} | "
f"Lon: {msg.longitude:0.6f} | "
f"Sats: {msg.num_sats} | "
f"Alt: {msg.altitude}m")
else:
print("[SEARCH] Waiting for satellite lock...")
except pynmea2.ParseError:
# Silently ignore sentences with corrupted checksums
pass
except serial.SerialException as e:
print(f"[HARDWARE ERROR] Failed to open serial port: {e}")
sys.exit(1)
except KeyboardInterrupt:
print("\n[INFO] Stream interrupted by user. Closing port.")
ser.close()
sys.exit(0)
if __name__ == "__main__":
parse_gps_stream()
Debugging: Fixing Permission and Lock Errors
When working with serial devices on Linux, you will inevitably hit permission or hardware lock errors. If your script crashes immediately, check the exact error string against the ranked causes below.
Error 1: "Permission denied"
Exact Error String: serial.serialutil.SerialException: [Errno 13] Permission denied: '/dev/ttyAMA0'
- Missing User Group (Most Likely): Your current user is not in the
dialoutgroup, which governs serial port access. Fix: Runsudo usermod -a -G dialout $USER, then log out and log back in. - Getty Service Hogging the Port: The system login shell is still attached to the UART. Fix: Run
sudo systemctl disable serial-getty@ttyAMA0.serviceand reboot. - Wrong Device Tree Overlay: Bluetooth is mapped to the primary UART. Fix: Add
dtoverlay=disable-btto/boot/firmware/config.txt.
Error 2: "No Fix" or Script Hangs
Symptom: The script runs, but prints [SEARCH] Waiting for satellite lock... indefinitely, or outputs nothing at all.
- Indoor Testing: The NEO-6M cannot see satellites through a standard roof. Fix: Take the Pi outside or place the antenna in a window with a clear view of the southern sky. Expect a cold start to take up to 5 minutes.
- Baud Rate Mismatch: If you previously connected the module to an Arduino and changed the baud rate via UBX commands, it will no longer be at 9600. Fix: Use the u-center software on a PC to factory reset the module, or cycle through 4800, 9600, and 115200 in the Python script.
- RX/TX Crossed: You wired TX to TX and RX to RX. Fix: Swap the yellow and green jumper wires. TX must always connect to RX.
- Run
ls -l /dev/ttyAMA0to verify the port exists and your user hasrwpermissions via thedialoutgroup. - Run
cat /dev/ttyAMA0in the terminal. If you see a scrolling wall of raw NMEA text ($GPGGA, $GPRMC), your hardware wiring and OS config are perfect; the issue is in your Python code. - Check the red PPS/Status LED on the NEO-6M breakout. If it is blinking at 1Hz, the module has a satellite fix. If it is solid or off, it is still searching or lacks power.
Extending and Simplifying the Build
Depending on your end goal, a raw UART Python script might not be the most efficient architecture for a production project. Here is how to adapt the build.
How to Simplify: Switch to I2C
If you are already using the Pi’s UART for a LoRaWAN concentrator or a 3D printer controller (like Klipper), you will run out of hardware UART ports. Simplify the build by switching to an I2C GPS module (such as the Adafruit Ultimate GPS I2C breakout). I2C operates on a shared bus (/dev/i2c-1 on Pi 5 GPIO 2/3), meaning you can daisy-chain the GPS alongside BME280 sensors and OLED displays without triggering serial port lock conflicts.
How to Extend: Implement the gpsd Daemon
If you have multiple Python scripts (e.g., one logging to a database, one updating a dashboard) trying to read /dev/ttyAMA0 simultaneously, the OS will throw a "Resource busy" error. Extend the build by installing gpsd (sudo apt install gpsd gpsd-clients).
The gpsd daemon runs in the background, claims the serial port, and serves the parsed GPS data over a local TCP socket (port 2947). Your Python scripts then use the gps library to query the daemon, allowing unlimited concurrent readers and providing automatic background caching of the last known fix when you drive through a tunnel.






