Pico: GPS location and time with the NEO-6M
Wire a NEO-6M GPS module to a Pico over UART, parse NMEA sentences in MicroPython for position and atomic-clock UTC time, and learn why the first fix takes minutes outdoors.
A NEO-6M module gives a Pico two things that are hard to get any other way: where it is, and what time it is, to the second, with no internet and no RTC battery to drift. The satellite part is free and never needs an account. On the Pico, which has no battery-backed clock, GPS is the cleanest answer to “what time is it really” for logging projects.
I put one on a windowsill the first time, watched the NMEA stream for five minutes, and started rewriting code I did not need to change. The module was fine. It was downloading the satellite almanac at 50 bits per second, which is exactly what GPS modules do after a cold start, and the fix landed two minutes after I carried the thing outside. The lesson: a GPS module is not an I2C sensor. There is no “read the register” moment. It streams sentences over serial and you wait for a valid fix, and indoors it may never come.
The other trap is the pin labels. The four pins on the little blue board are printed in 1.5 mm letters and everyone wires TX to TX once. Crossed, always: the module’s TX goes to the Pico’s RX.
What you need
Needed
- Raspberry Pi Pico (any version; no Wi-Fi needed for this one)
- NEO-6M GPS module with the ceramic patch antenna (u-blox on the blue
- Jumper wires, female-female if your module has male header pins
- A breadboard helps but is not required
Nice to have
- A soldering iron and solder (only if you solder the header pins yourself)
- Helping hands or a vise to hold the board while you work
- An anti-static wristband (cheap insurance for the RP2040)
Wiring
Serial only, two data wires plus power:
| NEO-6M pin | Pico |
|---|---|
VCC | 3.3V (pin 36) |
GND | GND (pin 38) |
TX | GPIO 1 (UART0 RX, physical pin 2) |
RX | GPIO 0 (UART0 TX, physical pin 1) |
Notice the cross: GPS TX to Pico RX (GP1), GPS RX to Pico TX (GP0). The rule is “my TX to your RX.” If both devices try to drive the same wire, you get silence and you will blame the code.
The NEO-6M is a 3.3V device and happy on the Pico’s 3.3V rail. The common blue breakout has a regulator, so 5V on VCC also works; the TX pin still swings 3.3V, which is what you want into a Pico GPIO. Bare modules without a breakout are a different story: check for the regulator chip before wiring.
Keep the GPS off GP0/GP1 only if you plan to use UART0 for a REPL on physical pins; over USB, which is what Thonny uses, GP0/GP1 are free and this wiring is fine.
Install
Nothing to install. MicroPython’s machine.UART reads the bytes, and
the NMEA parser below is 20 lines of plain Python. I have seen people
install a GPS library for the Pico, and every time the raw parse ended
up simpler than the library import.
The code
Save this as main.py on the Pico:
from machine import UART, Pin
import time
uart = UART(1, baudrate=9600, tx=Pin(4), rx=Pin(5))
# UART1 on GP4/GP5 keeps GP0/GP1 free for other things.
# If you wired to GP0/GP1 as in the table above, use:
# uart = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1))
def parse_gga(sentence):
# $GPGGA,time,lat,N,lon,E,fix,sats,hdop,alt,M,...
parts = sentence.split(',')
if len(parts) < 10 or not parts[2] or not parts[4]:
return None
def deg(val, hemi):
# NMEA latitude is ddmm.mmmm, longitude dddmm.mmmm
d = float(val[:2] if hemi in ('N', 'S') else val[:3])
m = float(val[2:] if hemi in ('N', 'S') else val[3:])
dec = d + m / 60.0
return -dec if hemi in ('S', 'W') else dec
fix = int(parts[6] or 0)
sats = int(parts[7] or 0)
if fix == 0:
return None
t = parts[1]
return {
"lat": deg(parts[2], parts[3]),
"lon": deg(parts[4], parts[5]),
"sats": sats,
"utc": f"{t[0:2]}:{t[2:4]}:{t[4:6]}",
"alt_m": parts[9],
}
buf = b""
while True:
if uart.any():
buf += uart.read()
while b"\n" in buf:
line, _, buf = buf.partition(b"\n")
sentence = line.decode("ascii", "ignore").strip()
# GGA has position + time; GNGGA on multi-constellation modules
if sentence.startswith(("$GPGGA", "$GNGGA")):
fix = parse_gga(sentence)
if fix:
print(f"Lat {fix['lat']:.6f} Lon {fix['lon']:.6f} "
f"Sats {fix['sats']} UTC {fix['utc']} "
f"Alt {fix['alt_m']}m")
time.sleep_ms(50)
Run it, then put the antenna somewhere it can see sky. A cold start
takes 1 to 15 minutes (the module downloads the almanac and remembers
it on its coin cell); after that, warm starts take seconds. When
Sats climbs past 4 you have a fix worth trusting.
The pattern in one sentence: bytes accumulate in a buffer, complete lines get parsed, and the GGA sentence carries position, altitude, and time together.
Why GPS time is worth the trouble
The Pico has no battery-backed clock. MicroPython’s time.localtime()
starts at zero every boot, and ntptime.settime() needs internet.
GPS hands you the actual time, every second, from an atomic-clock-backed
signal, with no network at all. For a logger in a greenhouse or a
crawlspace, that is the difference between “roughly when” and
“admissible evidence” (e.g. a freezer temperature log where the
timestamp has to hold up later).
Setting the Pico’s clock from the fix is one call:
import machine
def set_rtc_from_gps(fix):
# fix['utc'] is "HH:MM:SS"; date needs the RMC sentence
h, m, s = (int(x) for x in fix["utc"].split(":"))
machine.RTC().datetime((2026, 1, 1, 0, h, m, s, 0))
The date part needs the RMC sentence (parse $GPRMC the same way; the
date is field 9 in ddmmyy form). The microSD datalogger pairs well
here: GPS sets the clock at boot, the logger stamps rows with real
time, and the card keeps the history.
What you learned
- GPS gives position, altitude, and UTC time over plain serial at 9600 baud; no library needed to parse the GGA sentence.
- Cross the TX/RX wires. Everyone wires TX-to-TX once.
- First fix takes minutes outdoors and never works through a roof; indoors next to a window is a coin flip.
- GPS time is UTC. Your timezone offset is your job (e.g. Mountain time is UTC minus 7 in daylight saving, minus 6 in winter).
When something breaks
- Nothing on serial at all. TX and RX are swapped, or the module is not powered (the red LED should be lit). Swap the two wires and both problems usually go away at once.
- Serial shows gibberish. Baud mismatch. The NEO-6M ships at 9600
and some sellers reconfigure to 9600-plus-never-tell-you. Try 4800,
then 9600, then 19200, then 115200 in the
UART(...)call until$GPsentences appear. - No fix after 20 minutes indoors. Not a bug. Take it outside, or at least to a windowsill facing open sky. Metal roofs and parking garages block it entirely, and concrete-plus-rebar buildings are nearly as bad.
- Fix works, then drifts and drops. The u.FL antenna connector worked loose; it is the flimsiest part of the whole module. Reseat it, or put a drop of hot glue over the connector for permanent installs.
- Time is right but the date is 1980-something. The module has time but not the full almanac for the date. Wait outside; the date fills in once position locks.
- The Pico hangs when the GPS is plugged in. You wired the GPS to pins the REPL also uses, or you are on a bare 5V module without a regulator. Move to UART1 on GP4/GP5 and check the module for a regulator chip.
What to build next
- The Pico microSD datalogger pairs with this directly: GPS sets the clock, the card keeps the history, and you have a $20 standalone GPS logger.
- The Pico W MQTT tutorial streams the coordinates to a broker once a minute, which is the start of a geofence alert.
- The Pico asyncio tutorial runs the GPS reader, a status LED, and a web page at the same time, so you can watch the fix arrive from your phone in the yard.