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. |
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.
- 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?". - 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 - Disable the HCI UART service: This prevents the OS from trying to initialize Bluetooth on the serial port.
sudo systemctl disable hciuart - Reboot the Pi:
sudo reboot - Verify the Port: After rebooting, check your serial devices.
ls -l /dev/serial*
You should see/dev/serial0symlinked 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
pior 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.txthasdtoverlay=disable-bt. Runls -l /dev/serial*. Ifserial0points tottyS0, your overlay failed to load. Check for typos inconfig.txtand ensure you ransudo 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
pynmea2doesn'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
smbus2library 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
GGAdata into a local SQLite database or an InfluxDB time-series instance. Use thedatetimemodule 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.






