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.
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).
| 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.
- Open the terminal and run
sudo raspi-config. - Navigate to Interface Options > Serial Port.
- When asked "Would you like a login shell to be accessible over serial?", select No.
- When asked "Would you like the serial port hardware to be enabled?", select Yes.
- 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()
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:
- The
enable_uart=1flag 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. - The serial console is still active. If you skipped the
raspi-configstep, the Linux kernel is using/dev/serial0to output boot logs. Check/boot/firmware/cmdline.txtand remove the phraseconsole=serial0,115200if it exists. (Be careful not to delete the rest of the single-line file). - You are looking for
/dev/ttyAMA0instead of/dev/serial0. The/dev/serial0symlink is dynamically mapped to whichever UART is assigned to the GPIO pins. Hardcoding/dev/ttyAMA0in 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.






