The Direct Answer: Wiring a GPS Module to Raspberry Pi
If you need to add location tracking to your embedded project, connecting a GPS module to a Raspberry Pi via the hardware UART is the most reliable method. For this guide, we are targeting the Raspberry Pi 4 Model B paired with the Adafruit Ultimate GPS Breakout (MTK3339 chipset). We use this specific module because it natively operates at 3.3V logic, eliminating the need for logic level shifters that cheaper 5V modules require.
The direct answer for communication: you will read NMEA 0183 sentences from /dev/serial0 at a default baud rate of 9600. The MTK3339 updates at 1Hz by default, outputting standard GGA and RMC sentences containing latitude, longitude, speed, and UTC time.
Estimated Time: 45 minutes (hardware setup + software config + first satellite fix).
Required Parts List
- Board: Raspberry Pi 4 Model B (2GB, 4GB, or 8GB variant; running Raspberry Pi OS Bookworm or Bullseye 64-bit).
- GPS Module: Adafruit Ultimate GPS Breakout - 66 channel w/10 Hz updates (Product ID: 746, MTK3339 chipset). Typical street price: $40-$45.
- Antenna: Active GPS Patch Antenna with SMA connector (3m cable recommended for window placement).
- Wiring: 4x Female-to-Female jumper wires (silicone, 26 AWG).
Pin Mapping Table
Wire the GPS breakout to the Pi's 40-pin GPIO header. Remember the golden rule of serial communication: TX always connects to RX.
| GPS Breakout Pin | Raspberry Pi 4 GPIO Pin | Physical Pin # | Function |
|---|---|---|---|
| VIN | 3.3V Power | 1 | Module Power (3.3V-5V tolerant) |
| GND | Ground | 6 | Common Ground |
| TX | GPIO 15 (RXD) | 10 | GPS sends data to Pi |
| RX | GPIO 14 (TXD) | 8 | Pi sends config to GPS (Optional) |
Step-by-Step Hardware Setup & OS Configuration
The Raspberry Pi 4 routes the primary mini UART (/dev/ttyS0) to the GPIO header by default, aliased as /dev/serial0. However, the OS uses this serial port for the Linux console by default. You must disable the console login while keeping the hardware UART enabled.
- Wire the module according to the pin mapping table above. Connect the active SMA antenna to the GPS breakout and route the cable near a window or outdoors.
- Boot the Pi and open a terminal (or SSH in).
- Run the configuration tool: Type
sudo raspi-configand press Enter. - Navigate to:
Interface Options->Serial Port. - Answer the prompts:
- "Would you like a login shell to be accessible over serial?" -> Select No.
- "Would you like the serial port hardware to be enabled?" -> Select Yes.
- Exit and reboot: Select
Finishand reboot the Pi when prompted. - Verify the port: After reboot, run
ls -l /dev/serial*. You should see/dev/serial0pointing tottyS0.
For deeper technical details on how the Pi 4 handles UART routing versus the newer Pi 5 (which uses /dev/ttyAMA0 for the GPIO header), refer to the official Raspberry Pi UART documentation.
Complete Python Code for NMEA Parsing
While you can use the OS-level gpsd daemon, it introduces complex socket configurations and background service debugging that often trips up beginners. For standalone embedded projects, reading the serial stream directly and parsing NMEA sentences with Python is cleaner and more portable.
You will need two libraries. Install them via pip:
pip3 install pyserial pynmea2
Below is the complete, compilable Python script. It includes robust error handling for serial disconnects and malformed NMEA data, which is common during the first few seconds of a GPS boot sequence.
import serial
import pynmea2
import sys
import time
# --- PIN & PORT DEFINITIONS ---
# On Pi 4, /dev/serial0 maps to the GPIO UART (ttyS0)
SERIAL_PORT = '/dev/serial0'
BAUD_RATE = 9600
TIMEOUT = 2 # Seconds to wait for serial data
def parse_gps_stream():
"""Opens serial port and continuously parses NMEA GGA sentences."""
try:
ser = serial.Serial(
port=SERIAL_PORT,
baudrate=BAUD_RATE,
timeout=TIMEOUT,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS
)
print(f"Successfully opened {SERIAL_PORT} at {BAUD_RATE} baud.")
except serial.SerialException as e:
print(f"[FATAL] Failed to open serial port: {e}")
print("Check if /dev/serial0 exists and if you have dialout permissions.")
sys.exit(1)
try:
while True:
# Read a line from the serial buffer
raw_line = ser.readline()
# Decode bytes to string, ignoring malformed characters
try:
line = raw_line.decode('ascii', errors='replace').strip()
except UnicodeDecodeError:
continue
# We only care about sentences starting with $
if not line.startswith('$'):
continue
try:
msg = pynmea2.parse(line)
# GGA sentences contain the actual fix data (Lat/Lon/Alt)
if isinstance(msg, pynmea2.types.talker.GGA):
if msg.gps_qual == 0:
print("[STATUS] Waiting for satellite fix...")
else:
print(f"[FIX] Lat: {msg.latitude:.6f}, Lon: {msg.longitude:.6f} | "
f"Alt: {msg.altitude:.1f}m | Satellites: {msg.num_sats}")
except pynmea2.ParseError as e:
# The MTK3339 sometimes outputs proprietary sentences or
# corrupted bytes on startup. We catch and ignore these.
print(f"[WARN] Parse error on line: {line} -> {e}")
continue
except KeyboardInterrupt:
print("\n[INFO] GPS tracking stopped by user.")
finally:
if 'ser' in locals() and ser.is_open:
ser.close()
print("[INFO] Serial port closed.")
if __name__ == '__main__':
parse_gps_stream()
Debugging: When the Fix Fails or Code Crashes
GPS integration is notorious for failing silently. If your script runs but outputs nothing, or crashes immediately, follow this diagnostic tree.
The First Three Things to Check
- Is the serial console actually disabled? Run
cat /proc/cmdline | grep console. If you seeconsole=serial0,115200, the OS is hijacking your UART. Re-runraspi-config. - Is the antenna placed correctly? A GPS patch antenna cannot see satellites through a solid roof or low-E glass. It must have a clear line of sight to the sky. For bench testing, hang the antenna out an open window.
- Are TX and RX crossed? Verify that the GPS TX pin is wired to the Pi RX pin (GPIO 15). If they are backwards, the script will hang on
ser.readline()indefinitely.
Exact Error Strings and Ranked Causes
Error 1: serial.serialutil.SerialException: [Errno 13] Permission denied: '/dev/serial0'
- Cause A (Most Likely): Your current user is not in the
dialoutgroup, which owns the serial devices. - Fix: Run
sudo usermod -a -G dialout $USER, then log out and log back in. - Cause B: You are trying to access
/dev/ttyS0directly instead of the/dev/serial0alias, and the device tree hasn't mapped it correctly.
Error 2: pynmea2.ParseError: could not parse... checksum mismatch
- Cause A: Baud rate mismatch. The MTK3339 defaults to 9600. If you previously sent a command to change it to 115200 and the module saved it to EEPROM, your Python script will read garbage.
- Fix: Change
BAUD_RATE = 115200in the script, or perform a factory reset on the GPS module by sending the$PMTK104*37command via a serial terminal. - Cause B: Electrical noise on the TX line. Ensure your jumper wires are under 10cm long and not routed parallel to high-current DC motor wires.
Error 3: Script runs, but prints [STATUS] Waiting for satellite fix... forever.
- Cause A: The MTK3339 has a status LED. If it blinks every 15 seconds, it is searching. If it blinks once per second, it has a fix. If it is solid or off, check power.
- Cause B: Cold start delay. A GPS module with no saved ephemeris data takes 1 to 15 minutes to achieve a first fix (TTFF). Leave it running near a window for at least 10 minutes before assuming it's broken.
Extending and Simplifying the Build
Depending on your project constraints, you may want to scale this setup up for a vehicle tracker or down for a quick proof-of-concept.
How to Simplify (The USB Route)
If you want to skip GPIO wiring and UART configuration entirely, buy a u-blox VK-172 USB GPS Dongle (~$25). It plugs directly into a Pi USB port and mounts as a standard serial device (usually /dev/ttyACM0). You simply change the SERIAL_PORT variable in the Python script to /dev/ttyACM0, and you can completely skip the raspi-config UART steps. This is the best route for dashcam or marine navigation projects where USB ports are plentiful.
How to Extend (Data Logging & I2C Displays)
To turn this into a standalone tracker:
- Add an I2C OLED: Wire an SSD1306 128x64 OLED display to the Pi's I2C pins (SDA to GPIO 2, SCL to GPIO 3). Use the
adafruit-circuitpython-ssd1306library to render themsg.latitudeandmsg.longitudevariables directly to the screen, eliminating the need for an HDMI monitor. - CSV Logging: Import Python's
csvanddatetimemodules. Open a file in append mode inside thewhileloop and write a row containing the UTC timestamp, Lat, Lon, and Altitude. Wrap the file write in atry/except IOErrorblock to prevent the script from crashing if the SD card momentarily hangs.
Frequently Asked Questions
Why is my Raspberry Pi GPS module not getting a fix indoors?
GPS signals operate at extremely low power levels (around -130 dBm) by the time they reach Earth's surface. Standard building materials, especially concrete, metal roofing, and energy-efficient Low-E window coatings, block these L1 band (1575.42 MHz) signals entirely. You must use an active antenna and place it outdoors or directly against a standard glass window. If you require indoor tracking, you need to integrate a dead-reckoning IMU or rely on Wi-Fi/cellular triangulation, not GPS.
Can I use a NEO-6M GPS module with Raspberry Pi safely?
Yes, but only if you verify the logic levels. The u-blox NEO-6M chip itself is 3.3V, but the cheap breakout boards sold online often include a 5V voltage regulator and route the TX pin directly from a 5V logic buffer. If you measure 5V on the TX pin with a multimeter, you must use a logic level shifter or a simple resistor voltage divider to step it down to 3.3V before connecting it to the Pi's GPIO 15. Failing to do so will destroy the Pi's RX pin.
What is the difference between /dev/ttyS0 and /dev/serial0?
On the Raspberry Pi 4, /dev/ttyS0 is the actual mini UART hardware device. However, Raspberry Pi OS creates a symbolic link called /dev/serial0 that always points to the primary UART mapped to the GPIO header, regardless of whether the underlying hardware is the mini UART (ttyS0) or the PL011 UART (ttyAMA0). Always code against /dev/serial0 to ensure your Python script remains portable across different Pi models and OS updates. For more on this mapping, see the Adafruit GPS on Raspberry Pi guide.
How do I log GPS data to a file in the background without a monitor?
You can run the Python script as a systemd service. Create a file at /etc/systemd/system/gps-logger.service, define the ExecStart=/usr/bin/python3 /home/pi/gps_script.py, and set Restart=always. Enable it with sudo systemctl enable gps-logger. This ensures the script starts on boot and automatically restarts if the serial connection drops or the script crashes.






