Fork me on GitHub

C Library for Reading GPS data from a GlobalTop MTK3339


We purchased the Adafruit Ultimate GPS breakout board (Figure 1) several months ago, but finally got to the point of using it for something. This GPS breakout uses a GlobalTop MTK3339 GPS chip, comes with an SMA connector antenna, and also requires an SMA to uFL adapter (Figure 2) also available at Adafruit. You will also require an FTDI USB-to-TTL serial cable, which can be available at various vendors like Sparkfun, Adafruit, Amazon, DigiKey, etc.

To read the GPS data as fast as possible using the libev event API, we chose to write a C library from scratch as opposed to using a server like gpsd. We want our application to embed the reading of the GPS and display the GPS data to the OLED library using our libssd1306 library.

Adafruit Ultimate GPS Breakout Board Figure 1. Adafruit Ultimate GPS Breakout Board © Adafruit Industries

Adafruit SMA to uFL Connector Figure 2. Adafruit SMA to uFL Connector © Adafruit Industries

The code in this post is as of git tag 0.2 or commit fd14b8884510bc42c55127890d9bdd51bc1473a5 from Github.

The datasheets for the GPS chip (version PA6H) on the breakout board (version 3) can be found at Adafruit’s website and we have mirrored it here.

The GPS chip also accepts custom commands that start with the string $PMTK and the details of the commands can be found at Adafruit and we have mirrored it here.

CONNECTING THE GPS BREAKOUT BOARD

Using the USB-to-TTL Cable

The easiest way to connect and test the GPS breakout board is by using the FTDI USB-to-TTL serial cable.

Connecting the cable has to be done using the wiring in Figure 3.

FTDI USB-to-TTL Cable Connecting to GPS breakout board Figure 3. How to connect the FTDI USB-to-TTL Cable Connector to the Ultimate GPS Breakout board © Adafruit Industries

You will need some extra connecting wires to be able to connect the USB-to-TTL cable’s connectors to the GPS breakout board’s connecting pins.

Connecting via GPIO Pins on Raspberry Pi

If you want to directly connect the GPS breakout board to a Raspberry Pi or Beaglebone Black UART GPIO pins, you have to perform the wiring as in Figure 4.

UART GPIO pins to GPS breakout board Figure 4. How to connect the UART GPIO Pins on a Raspberry Pi to the Ultimate GPS Breakout board © Adafruit Industries

COMPILING THE LIBRARY

We have made it as simple as possible to compile the library.

## install pre-requisites
$ sudo apt-get -y install ragel libev-dev build-essential \
    autoconf automake autotools-dev libtool

## clone the library source code
$ git clone https://github.com/stealthylabs/libgps_mtk3339
$ git submodule init
$ git submodule update

## change to the source directory
$ cd libgps_mtk3339

## run the autotools
$ ./autogen.sh

## run configure in release mode
$ ./configure

## run make and make sure unit tests run
$ make && make check

## optionally run make install if you want to install the library
$ make install

READING THE DATA

This library’s main purpose is to be able to seamlessly, and with the least amount of effort, be able to read data from the MTK3339 GPS chip made by GlobalTop. Here for example, we are using the open source AdaFruit Ultimate GPS breakout board, but you could be using your own custom board with this chip and want to read the data faster than a Python script could.

That is why we have written this parser using one of the best parser generator tools, ragel, so that we can parse each individual NMEA sentence sent by the GPS in under 6-10 microseconds depending on the client computer.

The library is also re-entrant so that the user can use event libraries like libev or just a select() or epoll() or a blocking read() function call to read the data from the GPS chip when connected in any of the above two ways, shown in Figures 3 and 4. One such example we explain below.

Example with libev

We provide a usable example that prints location information on screen using libev. This example can be found in src/libev_uart.c file. In this section, we go over some important portions of the code to help the user understand how to use the library.

To run the example, you can do the following after compiling the code:

$ ./src/libev_uart_gps

This expects the device to be connected as /dev/ttyUSB0. You can also specify your own device path on the command line:

$ ./src/libev_uart_gps /dev/serial0

This example automatically exits after 10 seconds. However, sometimes the GPS chip may take more than 10 seconds to get a fix, so you can set the example to run without the timeout by setting the NO_TIMEOUT environment variable as shown below. You can then press Ctrl C to interrupt the running application.

$ NO_TIMEOUT=1 ./src/libev_uart_gps

Header Files

The user only needs to include the gpsdata.h header file which indirectly includes all the other dependency files such as:

  • gpsconfig.h: has the constants based on the build system
  • gpsutils.h: has some utility functions such as for opening the device and setting the defaults
  • gps_utlist.h: we provide a copy of the build version of utlist.h self-contained header/library from the utlist project. We use this to return a linked-list of parsed objects that the user can iterate through.

For this example, the user needs to also include ev.h to use libev.

#include <gpsdata.h>
/* ... other headers like errno.h, fcntl.h, stdio.h etc.... */
#ifdef HAVE_EV_H
    #include <ev.h>
#endif

Logging

The gpsutils.h file defines a set of macros for performing conditional logging at various levels. By default, the logging happens to stderr. The default log level is set to INFO and the user can change it using the GPSUTILS_LOGLEVEL_SET() macro as shown below.

// set the log level to DEBUG
GPSUTILS_LOGLEVEL_SET(DEBUG);

The user can also either reopen the stderr to a different log file using freopen(), or perform the following C-language macro-magic to set it to their favorite global FILE * variable, which they can set up later. NOTE: we do not do this in the src/libev_uart.c file, and this is for a special use case where the user cannot use freopen() on stderr.

/* use variable my_custom_log_ptr instead of stderr */
#define GPSUTILS_LOG_PTR my_custom_log_ptr
#include <gpsdata.h>

/* ... later in the code define the my_custom_log_ptr globally or locally to
every function before calling any GPSUTILS_LOG_* macro ... 
Here is a global pointer example
*/
FILE *my_custom_log_ptr = NULL;
/* ... main function ... */

int main(int argc, char **argv)
{
    my_custom_log_ptr = fopen("/path/to/log/file", "+w");
    /* ... more code ... */
    GPSUTILS_INFO("Starting log file...\n");
    return 0;
}

Opening the device

When using the USB-to-TTL cable on a Linux system, the device will show up as /dev/ttyUSB0 or in the device file format /dev/ttyUSB[0-9]. The src/libev_uart.c file assumes that the /dev/ttyUSB0 is the default, but also accepts a command-line argument to change that default to another device path.

When using the UART GPIO pins on a Raspberry Pi, the device path changes to /dev/serial or matches the /dev/serial[0-9] pattern.

To open the device, we provide a wrapper function that also sets the default chip baud rate to 9600 bps and the default NMEA sentences to GPRMC and GPGGA. This function is the gpsdevice_open() function and it returns the file descriptor after a successful open, or -1 on error. There is a corresponding gpsdevice_close() function that closes the open device. Below is a small snippet of code from the src/libev_uart.c file demonstrating the usage of these functions. We open the device in non-blocking mode so that we can read the data as fast as possible, but also handle other events when the data is not coming in.

int main (int argc, char **argv)
{
    /* ... */
    const char *dev = "/dev/ttyUSB0";
    int device_fd = gpsdevice_open(dev, true /* non-blocking */);
    if (device_fd < 0) {
        return -1;
    } else {
        /* use the device_fd here */
    }
    gpsdevice_close(device_fd);
    /* ... */
    return 0;
}

Creating the Parser

This library uses ragel, which is a streaming parser that uses state machines to parse data. It allows us to parse data read from the GPS chip, even if the data is incomplete in the buffer until the next buffer can be read. Not only that, it is also written in pure C which allows it to be really fast for our needs.

We encapsulate all the parser related code into an opaque object called gpsdata_parser_t and the user can create one using gpsdata_parser_create(). Each device must have its own gpsdata_parser_t object, as each file descriptor will be reading data which may be at a different state. In general, it is likely that you will be using only one GPS chip, but if you are using more, you need to have one parser object per GPS chip.

Once an array of bytes have been read from the GPS chip’s file descriptor, the function that parses it is gpsdata_parser_parse() which then returns a linked list of gpsdata_data_t objects which may have location information, antenna information, speed information, GPS chip firmware information and other possible messages.

The below code shows how to create the parser, and use the functions in blocking mode without libev:

/* create the parser object */
gpsdata_parser_t *parser = gpsdata_parser_create();
/* open device */
int device_fd = gpsdevice_open("/dev/ttyUSB0", false /* blocking */);
/* a buffer pointer to read data */
char buf[80];
ssize_t nb = 0;
gpsdata_data_t *listp = NULL;
/* blocking read */
while ((nb = read(device_fd, buf, sizeof(buf))) > 0) {
    size_t onum = 0;
    /* parse the data that was read and store outputs in listp
     *  onum holds the number of items added to listp
     */
    if (gpsdata_parser_parse(parser, buf, nb, &listp, &onum) < 0) {
        /* if the buffer had bad data in it, we reset the parser state
         * and allow the parser to continue working to parse the next set of
         *  messages.
         */
        gpsdata_parser_reset(parser);
    }
    printf("Parsed %zu NMEA messages in the buffer\n", onum);
    /** do something with the listp data like store in a database or
     * update a display on screen etc.
     */
}
/* close the device */
gpsdevice_close(device_fd);
/* free the listp  object */
gpsdata_list_free(listp);
/* free the parser object */
gpsdata_parser_free(parser);

Sending Messages to the GPS Device

We can also send several messages to the GPS device since it uses a UART interface for receiving data. The messages that can be sent are found in the PMTK command datasheet (also linked above).

We have taken most of the useful ones and wrapped them in their own functions, and also provide a low level message send function that allows the user to send their own custom message to the GPS device. All these functions begin with gpsdevice_ and use the file descriptor returned by gpsdevice_open() to send the messages to the GPS chip.

  • gpsdevice_set_baudrate(): Allows the user to set a custom baudrate for the GPS. The default value is set to be 9600 bps, and the other supported values are 1200, 2400, 4800, 9600, 19200, 38400, 57600 and 115200. As of this writing, we have only tested 9600 bps since that’s what the datasheet says we should use. This function gets called with the 9600 default value in the gpsdevice_open() call, but the user can change that by calling it again.
  • gpsdevice_set_restart(): Allows the user to perform different type of restarts as specified in the PMTK document linked above &em; the warm start, cold start and a full cold start or a factory reset.
  • gpsdevice_set_standby(): Sends a message to the GPS chip to be on standby. To exit standby mode the user can send any message to the GPS chip, such as an enable message or a firmware or antenna request or anything else.
  • gpsdevice_set_fix_interval(): Tells the GPS the fix interval in milliseconds that it should use. For a 9600 baud this should be 1000 milliseconds or 1 second. The minimum value is 100 and maximum value accepted is 10000 milliseconds.
  • gpsdevice_set_enabled(): By default the only NMEA messages enabled are GPGGA and GPRMC since they give us the location which is enough for most uses. The user may want to enable other NMEA sentences like GPVTG, GPGSA and GPGSV. To do that the user may want to use this function. As of this writing, GPGLL is disabled at all times as it is unnecessary. We have tested enabling GPGLL, GPGGA and GPRMC simultaneously and the value of the location is the same, hence have deemed GPGLL unnecessary.
  • gpsdevice_set_speed_threshold(): The navigation speed has a minimum accuracy threshold that can be set here. By default, it is set to 0.2 m/s. The valid values as per the data sheet are 0.2, 0.4, 0.6, 0.8, 1.0, 1.5 and 2.0 m/s. If the user wants to disable the threshold they can use 0. If the speed is slower than the threshold the location is considered constant or frozen.
  • gpsdevice_request_firmware_info(): The user can request the firmware version that the GPS chip is running. The data gets returned and parsed by the gpsdata_parser_parse() function and the firmware can be read in the gpsdata_data_t object using the fwinfo field, and the msgid will be GPSDEVICE_MSGID_PMTK.
  • gpsdevice_request_antenna_status(): The user can request antenna status of the chip. If the user is using the Adafruit GPS breakout board like us, we are using the external antenna, which will set the antenna_status field in the gpsdata_data_t object as GPSDATA_ANTENNA_ACTIVE. If the user is using the chip’s internal antenna, the antenna status will be GPSDATA_ANTENNA_INTERNAL.
  • gpsdevice_send_message(): This is the core message send function which is used by all the wrappers internally. The user can set their own PMTK message if any of our wrappers are insufficient for their use case.

Using the Device with libev

The sample application present in src/libev_uart.c can be used as a starting point for using libev and the libgps_mtk3339 library. Remember to open the device in non-blocking mode and to create one parser per opened device.

With this we come to the end of the introduction to the libgps_mtk3339 library. Try it out instead of using the gpsd daemon or the Adafruit Python libraries and let us know if you have any issues.