You can easily use a GPS module like the ultimate GPS FeatherWing with CircuitPython code. Python code is well suited for parsing and processing the text output from GPS modules and this Adafruit CircuitPython GPS module handles most of the work for you!
First make sure your Feather is running CircuitPython firmware, and the GPS FeatherWing is assembled and connected to the Feather board. By default the FeatherWing will automatically use the Feather's RX and TX pins for a hardware serial connection.
Note: Just like with Arduino the ESP8266 and nRF52832 Feather shares its serial RX and TX pins with the USB serial connection and can be difficult to use with GPS modules. In particular you won't be able to use the REPL and other core tools like ampy to copy files if a GPS FeatherWing is connected. It's recommended not to use the ESP8266 or nRF52832 with the GPS FeatherWing.
Next you'll need to install the Adafruit CircuitPython GPS library on your CircuitPython board
First make sure you are running the latest version of Adafruit CircuitPython for your board.
Next you'll need to install the necessary libraries to use the hardware--carefully follow the steps to find and install these libraries from Adafruit's CircuitPython library bundle. For example the Circuit Playground Express guide has a great page on how to install the library bundle for both express and non-express boards.
Remember for non-express boards like the Trinket M0, Gemma M0, and Feather/Metro M0 basic you'll need to manually install the necessary libraries from the bundle:
- adafruit_gps.mpy
You can also download the adafruit_gps.mpy file from the Adafruit CircuitPython GPS releases page.
Before continuing make sure your board's lib folder or root filesystem has the adafruit_gps.mpy, files and folders copied over.
Usage
To demonstrate the usage of the GPS module in CircuitPython let's look at a complete program example. the simple.py file from the module's examples. Save this file as main.py on your board, then open the REPL connection to the board to see its output:
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries # SPDX-License-Identifier: MIT # Simple GPS module demonstration. # Will wait for a fix and print a message every second with the current location # and other details. import time import board import busio import adafruit_gps # Create a serial connection for the GPS connection using default speed and # a slightly higher timeout (GPS modules typically update once a second). # These are the defaults you should use for the GPS FeatherWing. # For other boards set RX = GPS module TX, and TX = GPS module RX pins. uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) # for a computer, use the pyserial library for uart access # import serial # uart = serial.Serial("/dev/ttyUSB0", baudrate=9600, timeout=10) # If using I2C, we'll create an I2C interface to talk to using default pins # i2c = board.I2C() # uses board.SCL and board.SDA # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller # Create a GPS module instance. gps = adafruit_gps.GPS(uart, debug=False) # Use UART/pyserial # gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface # Initialize the GPS module by changing what data it sends and at what rate. # These are NMEA extensions for PMTK_314_SET_NMEA_OUTPUT and # PMTK_220_SET_NMEA_UPDATERATE but you can send anything from here to adjust # the GPS module behavior: # https://cdn-shop.adafruit.com/datasheets/PMTK_A11.pdf # Turn on the basic GGA and RMC info (what you typically want) gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") # Turn on the basic GGA and RMC info + VTG for speed in km/h # gps.send_command(b"PMTK314,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") # Turn on just minimum info (RMC only, location): # gps.send_command(b'PMTK314,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') # Turn off everything: # gps.send_command(b'PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') # Turn on everything (not all of it is parsed!) # gps.send_command(b'PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0') # Set update rate to once a second (1hz) which is what you typically want. gps.send_command(b"PMTK220,1000") # Or decrease to once every two seconds by doubling the millisecond value. # Be sure to also increase your UART timeout above! # gps.send_command(b'PMTK220,2000') # You can also speed up the rate, but don't go too fast or else you can lose # data during parsing. This would be twice a second (2hz, 500ms delay): # gps.send_command(b'PMTK220,500') # Main loop runs forever printing the location, etc. every second. last_print = time.monotonic() while True: # Make sure to call gps.update() every loop iteration and at least twice # as fast as data comes from the GPS unit (usually every second). # This returns a bool that's true if it parsed new data (you can ignore it # though if you don't care and instead look at the has_fix property). gps.update() # Every second print out current location details if there's a fix. current = time.monotonic() if current - last_print >= 1.0: last_print = current if not gps.has_fix: # Try again if we don't have a fix yet. print("Waiting for fix...") continue # We have a fix! (gps.has_fix is true) # Print out details about the fix like location, date, etc. print("=" * 40) # Print a separator line. print( "Fix timestamp: {}/{}/{} {:02}:{:02}:{:02}".format( gps.timestamp_utc.tm_mon, # Grab parts of the time from the gps.timestamp_utc.tm_mday, # struct_time object that holds gps.timestamp_utc.tm_year, # the fix time. Note you might gps.timestamp_utc.tm_hour, # not get all data like year, day, gps.timestamp_utc.tm_min, # month! gps.timestamp_utc.tm_sec, ) ) print("Latitude: {0:.6f} degrees".format(gps.latitude)) print("Longitude: {0:.6f} degrees".format(gps.longitude)) print( "Precise Latitude: {} degs, {:2.4f} mins".format( gps.latitude_degrees, gps.latitude_minutes ) ) print( "Precise Longitude: {} degs, {:2.4f} mins".format( gps.longitude_degrees, gps.longitude_minutes ) ) print("Fix quality: {}".format(gps.fix_quality)) # Some attributes beyond latitude, longitude and timestamp are optional # and might not be present. Check if they're None before trying to use! if gps.satellites is not None: print("# satellites: {}".format(gps.satellites)) if gps.altitude_m is not None: print("Altitude: {} meters".format(gps.altitude_m)) if gps.speed_knots is not None: print("Speed: {} knots".format(gps.speed_knots)) if gps.speed_kmh is not None: print("Speed: {} km/h".format(gps.speed_kmh)) if gps.track_angle_deg is not None: print("Track angle: {} degrees".format(gps.track_angle_deg)) if gps.horizontal_dilution is not None: print("Horizontal dilution: {}".format(gps.horizontal_dilution)) if gps.height_geoid is not None: print("Height geoid: {} meters".format(gps.height_geoid))
When the code runs it will print a message every second to the REPL, either an update that it's still waiting for a GPS fix:
Or once a fix has been established (make sure the GPS module has a good view of the sky!) it will print details about the current location and other GPS data:
Let's look at the code in a bit more detail to understand how it works. First the example needs to import a few modules like the built-in busio and board modules that access serial ports and other hardware:
import board import busio import time
Next the GPS module is imported:
import adafruit_gps
Now a serial UART is created and connected to the serial port pins the GPS module will use, this is the low level transport layer to communicate with the GPS module:
# Define RX and TX pins for the board's serial port connected to the GPS. # These are the defaults you should use for the GPS FeatherWing. # For other boards set RX = GPS module TX, and TX = GPS module RX pins. RX = board.RX TX = board.TX # Create a serial connection for the GPS connection using default speed and # a slightly higher timeout (GPS modules typically update once a second). uart = busio.UART(TX, RX, baudrate=9600, timeout=3000)
Once a UART object is available with a connected GPS module you can create an instance of the GPS parsing class. You need to pass this class the UART instance and it will internally read new data from the GPS module connected to it:
gps = adafruit_gps.GPS(uart)
Before reading GPS data the example configures the module by sending some custom NMEA GPS commands that adjust the amount and rate of data. Read the comments to see some options for adjust the rate and amount of data, but typically you want the defaults of core location info at a rate of once a second:
# Initialize the GPS module by changing what data it sends and at what rate. # These are NMEA extensions for PMTK_314_SET_NMEA_OUTPUT and # PMTK_220_SET_NMEA_UPDATERATE but you can send anything from here to adjust # the GPS module behavior: # https://cdn-shop.adafruit.com/datasheets/PMTK_A11.pdf # Turn on the basic GGA and RMC info (what you typically want) gps.send_command(b'PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') # Turn on just minimum info (RMC only, location): #gps.send_command(b'PMTK314,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') # Turn off everything: #gps.send_command(b'PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') # Tuen on everything (not all of it is parsed!) #gps.send_command(b'PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0') # Set update rate to once a second (1hz) which is what you typically want. gps.send_command(b'PMTK220,1000') # Or decrease to once every two seconds by doubling the millisecond value. # Be sure to also increase your UART timeout above! #gps.send_command(b'PMTK220,2000') # You can also speed up the rate, but don't go too fast or else you can lose # data during parsing. This would be twice a second (2hz, 500ms delay): #gps.send_command(b'PMTK220,500')
If you want you can send other custom commands to the GPS module with the send_command function shown above. You don't need to worry about adding a NMEA checksum to your command either, the function will do this automatically (or not, set add_checksum=False as a parameter and it will skip the checksum addition).
Now we can jump into a main loop that continually updates data from the GPS module and prints out status. The most important part of this loop is calling the GPS update function:
# Make sure to call gps.update() every loop iteration and at least twice # as fast as data comes from the GPS unit (usually every second). # This returns a bool that's true if it parsed new data (you can ignore it # though if you don't care and instead look at the has_fix property). gps.update()
Like the comments mention you must call updated every loop iteration and ideally multiple times a second. Each time you call update it allows the GPS library code to read new data from the GPS module and update its state. Since the GPS module is always sending data you have to be careful to constantly read data or else you might start to lose data as buffers are filled.
You can check the has_fix property to see if the module has a GPS location fix, and if so there are a host of attributes to read like latitude and longitude (available in degrees):
if not gps.has_fix: # Try again if we don't have a fix yet. print('Waiting for fix...') continue # We have a fix! (gps.has_fix is true) # Print out details about the fix like location, date, etc. print('=' * 40) # Print a separator line. print('Fix timestamp: {}/{}/{} {:02}:{:02}:{:02}'.format( gps.timestamp_utc.tm_mon, # Grab parts of the time from the gps.timestamp_utc.tm_mday, # struct_time object that holds gps.timestamp_utc.tm_year, # the fix time. Note you might gps.timestamp_utc.tm_hour, # not get all data like year, day, gps.timestamp_utc.tm_min, # month! gps.timestamp_utc.tm_sec)) print('Latitude: {} degrees'.format(gps.latitude)) print('Longitude: {} degrees'.format(gps.longitude)) print('Fix quality: {}'.format(gps.fix_quality)) # Some attributes beyond latitude, longitude and timestamp are optional # and might not be present. Check if they're None before trying to use! if gps.satellites is not None: print('# satellites: {}'.format(gps.satellites)) if gps.altitude_m is not None: print('Altitude: {} meters'.format(gps.altitude_m)) if gps.track_angle_deg is not None: print('Speed: {} knots'.format(gps.speed_knots)) if gps.track_angle_deg is not None: print('Track angle: {} degrees'.format(gps.track_angle_deg)) if gps.horizontal_dilution is not None: print('Horizontal dilution: {}'.format(gps.horizontal_dilution)) if gps.height_geoid is not None:
Notice some of the attributes like altitude_m are checked to be None before reading. This is a smart check to put in your code too because those attributes are sometimes not sent by a GPS module. If an attribute isn't sent by the module it will be given a None/null value and attempting to print or read it in Python will fail. The core attributes of latitude, longitude, and timestamp are usually always available (if you're using the example as-is) but they might not be if you turn off those outputs with a custom NMEA command!
That's all there is to reading GPS location with CircuitPython code!
Datalogging Example
Another handy task with GPS is logging all the raw output of the GPS module to a file. This is useful if you're importing the GPS data into a tool like Google Earth which can process raw NMEA sentences. You can perform this datalogging very easily with CircuitPython.
To store data you'll need to choose one of two options:
- Wire up a SD card holder to your board's SPI bus, or use a board with SD card holder built-in like the Feather M0 Adalogger. This is the recommended approach as it gives you a lot of space to store data and you can easily copy the data to your computer from the card.
- Store data in your board's internal filesystem. This requires a little more setup but allows you to save to a file on the internal filesystem of your CircuitPython board, right next to where code and other data files live. This is more limited because depending on your board you might only have a few kilobytes or megabytes of space available and GPS sentences will quickly add up (easily filling multiple megabytes within a few hours of logging).
Install SD Card Library
If you're storing data on a SD card you must ensure the SD card is wired to your board and you have installed the Adafruit SD card library. Luckily there's an entire guide to follow to learn about this process of connecting a SD card and installing the necessary library. Be sure to carefully follow the guide so the card is connected, library installed, and you can confirm you're able to manually write data to the card from the Python prompt.
Enable Internal Filesystem Writes
If you're storing data on the internal filesystem you must carefully follow the steps in the CPU temperature logging guide to enable writing to internal storage. If you're writing to a SD card skip these steps and move on to look at the datalogging code below. Edit the boot.py on your board (creating it if it doesn't exist) and add these lines:
import digitalio import board import storage switch = digitalio.DigitalInOut(board.D5) switch.direction = digitalio.Direction.INPUT switch.pull = digitalio.Pull.UP # If the D5 is connected to ground with a wire # you can edit files over the USB drive again. storage.remount("/", not switch.value)
Remember once you remount("/") you cannot edit code over the USB drive anymore! That means you can't edit boot.py which is a bit of a conundrum. So we configure the boot.py to selectively mount the internal filesystem as writable based on a switch or even just alligator clip connected to ground. Like the CPU temperature guide shows . In this example we're using D5 but select any available pin.
This code will look at the D5 digital input when the board starts up and if it's connected to ground (use an alligator clip or wire, for example, to connect from D5 to board ground) it will disable internal filesystem writes and allow you to edit code over the USB drive as normal. Remove the alligator clip, reset the board, and the boot.py will switch to mounting the internal filesystem as writable so you can log images to it again (but not write any code!).
Remember when you enable USB drive writes (by connecting D5 to ground at startup) you cannot write files to the internal filesystem and any code in your main.py that attempts to do so (like the example below) will fail. Keep this in mind as you edit code--once you modify code you need to remove the alligator clip, reset the board to re-enable internal filesystem writes, and then watch the output of your program.
Datalogging Example Code
The GPS library examples have a datalogging.py file you can edit and save as a main.py on your board:
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries # SPDX-License-Identifier: MIT # Simple GPS datalogging demonstration. # This example uses the GPS library and to read raw NMEA sentences # over I2C or UART from the GPS unit and dumps them to a file on an SD card # (recommended), microcontroller internal storage (be careful as only a few # kilobytes are available), or to a filesystem. # If you are using a microcontroller, before writing to internal storage you # MUST carefully follow the steps in this guide to enable writes to the # internal filesystem: # https://learn.adafruit.com/adafruit-ultimate-gps-featherwing/circuitpython-library import sys import board import busio import adafruit_gps # Path to the file to log GPS data. By default this will be appended to # which means new lines are added at the end and all old data is kept. # Change this path to point at internal storage (like '/gps.txt') or SD # card mounted storage ('/sd/gps.txt') as desired. LOG_FILE = "gps.txt" # Example for writing to internal path gps.txt # File more for opening the log file. Mode 'ab' means append or add new lines # to the end of the file rather than erasing it and starting over. If you'd # like to erase the file and start clean each time use the value 'wb' instead. LOG_MODE = "ab" # sdcardio and adafruit_sdcard are NOT supported on blinka. If you are using a # Raspberry Pi or other single-board linux computer, the code will save the # output to the path defined in LOG_FILE above. if sys.platform != "linux": import storage SD_CS_PIN = board.D10 # CS for SD card using Adalogger Featherwing try: import sdcardio sdcard = sdcardio.SDCard(board.SPI, SD_CS_PIN) except ImportError: import adafruit_sdcard import digitalio sdcard = adafruit_sdcard.SDCard( board.SPI(), digitalio.DigitalInOut(SD_CS_PIN), ) vfs = storage.VfsFat(sdcard) storage.mount(vfs, "/sd") # Mount SD card under '/sd' path in filesystem. LOG_FILE = "/sd/gps.txt" # Example for writing to SD card path /sd/gps.txt # Create a serial connection for the GPS connection using default speed and # a slightly higher timeout (GPS modules typically update once a second). # These are the defaults you should use for the GPS FeatherWing. # For other boards set RX = GPS module TX, and TX = GPS module RX pins. uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) # If using a USB/Serial converter, use pyserial and update the serial # port name to match the serial connection for the GPS! # import serial # uart = serial.Serial("/dev/ttyUSB0", baudrate=9600, timeout=10) # If using I2C, we'll create an I2C interface to talk to using default pins # i2c = board.I2C() # uses board.SCL and board.SDA # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller # Create a GPS module instance. gps = adafruit_gps.GPS(uart) # Use UART/pyserial # gps = adafruit_gps.GPS_GtopI2C(i2c) # Use I2C interface # Main loop just reads data from the GPS module and writes it back out to # the output file while also printing to serial output. with open(LOG_FILE, LOG_MODE) as outfile: while True: sentence = gps.readline() if not sentence: continue print(str(sentence, "ascii").strip()) outfile.write(sentence) outfile.flush()
By default this example expects to log GPS NMEA sentences to a file on the internal storage system at /gps.txt. New sentences will be appended to the end of the file every time the example starts running.
If you'd like to instead write to the SD card take note to uncomment the appropriate lines mentioned in the comments:
# Path to the file to log GPS data. By default this will be appended to # which means new lines are added at the end and all old data is kept. # Change this path to point at internal storage (like '/gps.txt') or SD # card mounted storage ('/sd/gps.txt') as desired. #LOG_FILE = '/gps.txt' # Example for writing to internal path /gps.txt LOG_FILE = '/sd/gps.txt' # Example for writing to SD card path /sd/gps.txt
And further below:
# If writing to SD card customize and uncomment these lines to import the # necessary library and initialize the SD card: SD_CS_PIN = board.SD_CS # CS for SD card (SD_CS is for Feather Adalogger) import adafruit_sdcard spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) sd_cs = digitalio.DigitalInOut(SD_CS_PIN) sdcard = adafruit_sdcard.SDCard(spi, sd_cs) vfs = storage.VfsFat(sdcard) storage.mount(vfs, '/sd') # Mount SD card under '/sd' path in filesystem.
Should all be uncommented and look as above. This will configure the code to write GPS NMEA data to the /sd/gps.txt file, appending new data to the end of the file.
Once the example is running as a main.py on your board open the serial REPL and you should see the raw NMEA sentences printed out:
Check the gps.txt file (either under the root or /sd path depending on how you setup the example) in a text editor and you'll see the same raw NMEA sentences:
Awesome! That's all there is to basic datalogging of NMEA sentences with a GPS module and CircuitPython!
Page last edited January 22, 2025
Text editor powered by tinymce.