Return to Robotics Tutorials

GPS with SPI interface

u-blox NEO-M8N

Robots often integrate a GPS module to enable navigation. The majority of integrated GPS receivers provide a UART interface, but unfortunately this is not a convenient sensor interface for robots integrating a multitude of functions. This article explains how to make a SPI interface for your GPS using an ATtiny as a UART-to-SPI bridge.

GPS Receiver interfaces

There is a wide range of options available for robot GPS receiver modules. The absolute cheapest of the integrated modules (including antenna and breakout board / housing+cable) typically cost about U$8 and up (eg. u-blox NEO-6M). While these popular u-blox ICs provide a number of interfaces including UART, USB, I2C and SPI, the cheap breakout boards / shells often just export headers/cables for the UART interface since that is the common interface typically accepted by flight controllers (eg. APM, Pixhawk).

In the case of the u-blox NEO-M8N GPS module pictured at the right, the cable supplies a UART interface for the GPS and an I2C interface for the integrated compass (HMC5883L).

Unlike SPI and I2C, a single UART interface cannot easily communicate with multiple sensors. As the number of devices interacting with a robot grows, the greater the chance that multiple devices will expect to communicate via UART. Most microcontrollers only provide a single hardware UART interface, so this quickly presents a problem.

If we convert the GPS UART to SPI or I2C, then we can make the GPS appear to the main processor like just another device that shares the communication bus.

GPS logging on Raspberry Pi via SPI interface from ATtiny

UART-to-SPI Bridge with ATtiny

It quickly became apparent that having a SPI interface for the GPS module would be useful in minimizing the precious IO interfaces of the processor. In my case, my primary CPU for the robot is a Raspberry Pi 3. Using a SPI interface for the GPS meant that the controller can poll the GPS for new coordinates (ie. on-demand), rather than having to continuously process the incoming UART stream.

There are a couple off-the-shelf UART-to-SPI bridges (such as the NXP SC16IS7xx), but building a UART-to-SPI bridge is relatively easy to do with an Arduino or AVR microcontroller. I chose the ATtiny167 (Digispark Pro clone) for this purpose as the entire development board costs only U$3 shipped! This particular ATtiny has built-in hardware for the SPI interface as well as the UART, saving us from having to implement bit-banging in the firmware.

One complication in building the bridge is that we need to ensure the SPI slave accesses to the UART GPS data are coherent -- that is, we need to prevent the SPI master from reading a GPS value in the ATtiny while it has been partially updated by the GPS. See the section below on SPI slave coherency.

GPS with SPI interface using ATtiny

In the end-to-end system, I have a Raspberry Pi 3 which needs to read the coordinates from the GPS several times a second. This is accomplished in the following steps:

  • GPS outputs continous stream of coordinates via UART
    The u-blox NEO-M8N I purchased defaults to output position data approximately 5 times a second (5Hz) without any configuration. This meant that I do not need to supply a UART connection from the CPU into the GPS, only the other way around. The advantage here is that one fewer connection is required.
  • ATtiny monitors the incoming UART Rx interface
  • ATtiny parses the GPS coordinates (NMEA messages)
  • ATtiny saves GPS coordinates into holding registers
  • ATtiny implements coherent SPI slave access to the holding registers
  • Raspberry Pi (or other CPU) has a SPI master which reads the ATtiny holding registers

ATtiny UART receive

It is a trivial matter to implement the UART slave interface in the ATtiny with the built-in library functions: Serial.available() and Serial.read(). First, we need to start by configuring the UART to run at the baud rate of the GPS module; in the case of the M8N module I have, the rate is 38400 baud.

#define UART_RATE 38400	// Baud rate of GPS

void setup() {
  // ... other config here ...
  Serial.begin(UART_RATE);
}					
					

The Serial library responds to the signal transitions at the UART pin interface and transfers each byte to a receive buffer. It is then the responsibility of the software to read this buffer frequently enough to avoid having the buffer overflow (which would corrupt the read data). In the case of the UART-to-SPI bridge for the SPI-based GPS, we pass each byte received from the UART into a minimal parsing function. The output of the parsing function then passes the resulting values into a set of GPS sensor variables.

If the UART bit rate is low enough, one can perform the UART monitoring function in the main loop as shown below. Alternately, the interrupt handler for the UART can be used to perform these transfer & parsing functions if they are kept fast.

void loop() {
  // ...	
  int    nUartIn;
  if (Serial.available()) {
    nUartIn = Serial.read();
    if (nUartIn != -1) {
      // Process the UART RX byte (nUartIn)
      // ...
      ParseGPS(nUartIn);
      // ...
    }
  }
  // ...
}
					

Minimal GPS parsing

If you connect up a typical GPS module to a UART monitor (eg. Bus Pirate or PL2303), you will see a series of cryptic strings that generally start with a dollar-sign ($). These are the GPS NMEA sentences that report a vast array of data associated with the GPS satellites and their qualitative measures. Most robot implementations only need to parse a relatively small subset of the information reported in the NMEA protocol. For example, some robots only use the GPS to fetch the latest GPS coordinate fix, while others may use the coordinates in addition to the highly-accurate time synchronization functions.

$GNGSA,A,3,87,71,,,,,,,,,,,2.20,1.18,1.86*12
$GPGSV,4,1,15,02,61,05,26,05,3,138,32,0,15,054,1,09,02,01,*7F
$GGSV,4,2,1,12,52,16,32,19,00093,,20,0,180,,21,0,232,*7C
$GPGSV,4,,15,46,28148,,48,3,193,31,1,32,159,4531,22,07,18*7B
$GLGS,2,1,05,7,07,010,1,71,30,05,29,72,25114,21,7845,300,*6
$GLGSV2,2,05,8767,017,215B
$GNGL,3725.5402,N,1220508739,W,03123.00,AD*61
$GNRMC,023123.20,A,3725.54083,N,12205.08740,W,0.064,,240516,,,D*73
$GNVTG,,T,,M,0.064,N,0.118,K,D*32
$GNGGA,023123.20,3725.54083,N,12205.08740,W,2,09,1					
Example GPS NMEA messages

To keep the SPI register interface simple, I only map over a few of the more common fields output by the GPS:

  • GPS Coordinate (Latitude and Longitude)
  • Ground speed
  • Heading (course)
  • Date
  • Time

These fields are typically output in the RMC (Recommended Minimum Data) message. According to the u-blox M8N receiver protocol specification, the RMC message is defined as follows:

Field No.NameFormatExampleDescription
0xxRMCstring$GPRMCRMC Message ID (xx = current Talker ID)
1timehhmmss.ss083559.00UTC time
2statuscharacterAStatus, V=Navigation receiver warning, A=Data valid
3latddmm.mmmmm4717.11437Latitude (degrees & minutes)
4NScharacterNNorth/South indicator
5longdddmm.mmmmm00833.91522Longitude (degrees & minutes)
6EWcharacterEEast/West indicator
7spdnumeric0.004Speed over ground
8cognumeric77.52Course over ground
9dateddmmyy091202Date in day, month, year format

In order to decode these strings, I leveraged a handy snippet of code from David Johnson-Davies entitled Minimal GPS Parser. His ParseGPS() function accepts a single character at a time (well-suited to UART handlers) and steps through a simple state machine defined by a format string. This clever implementation uses pattern matching to determine which field we are parsing and then converts and stores the result in the corresponding variable.

The code is largely based on David's work but modified slightly to add support for checksum calculations and GNSS modules (such as the NEO-M8N). I encourage readers to take a look at David's site to see a number of other interesting projects involving the ATtiny85.

NOTE: The following listing has been compressed for brevity. The full listing (available for download) includes all the associated commented code.

// --------------------------------------------------
// GPS decoding
// - Based on "Minimal GPS Parser" code by David Johnson-Davies
//   http://www.technoblogy.com/show?SJ0
// - Modifications by Calvin Hass ( http://www.impulseadventure.com/elec/ ):
// - - Added checksum calculation and optional field handling
// - - Changed format to support $GNRMC and checksum
// - - Other misc changes (naming, initialization, etc.)

// Configuration:

// The following defines the GPS RMC message that we will parse and extract
//
// Example (GPRMC):  "$GPRMC,194509.000,A,4042.6142,N,07400.4168,W,2.03,221.11,160412,,,A*77"
//         (GNRMC):  "$GNRMC,023123.20,A,3725.54083,N,12205.08740,W,0.064,,240516,,,D*73"
//
// Uncomment one of the two following lines, depending on which RMC message
// is expected from the GPS unit. The skip field offsets may also need adjusting.
//
//char m_acGpsFmt[]="$GPRMC,dddtdd.ddm,A,eeae.eeee,l,eeeae.eeee,o,djdk,ddd.dc,dddy??,,,?z#x";
  char m_acGpsFmt[]="$GNRMC,dddtdd.dm,A,eeae.eeeee,l,eeeae.eeeee,o,djddk,ddd.dc,dddy??,,,?z#x";

                   
// GPS temp variables
volatile uint8_t        m_nGpsState = 0;
unsigned int            m_nTemp;
long                    m_nLTemp;

// GPS checksum calculation
volatile boolean    m_bGpsChkEn = 0;
volatile uint8_t    m_nGpsChkSum = 0;
volatile uint8_t    m_nGpsChkVal = 0;

// GPS output variables
volatile unsigned int   m_nGpsTime, m_nGpsMsecs, m_nGpsKnots, m_nGpsCourse, m_nGpsDate;
volatile long           m_nGpsLat, m_nGpsLong;
volatile boolean        m_bGpsFix = 0;
volatile bool           m_bGpsValid = 0;
volatile boolean        m_bGpsChkOk = 0;


void ParseGPS (char nCh) {
  if (nCh == '$') { m_nGpsState = 0; m_nTemp = 0; m_nLTemp = 0; m_bGpsChkEn = 0; m_bGpsChkOk = 0; }

  // Handle any fields that might be skipped in output
  if ((m_nGpsState == 52) && (nCh == ',')) { m_nGpsState += 6; }  // Skip COURSE in $GNRMC

  char cMode = m_acGpsFmt[m_nGpsState++];
  // d=decimal digit
  char nDigit = nCh - '0';
  // #=hexadecimal (uppercase) digit
  char nHex = (nCh>='A')?(nCh-'A'+10):(nCh-'0');
  if (cMode == 'd') m_nTemp = m_nTemp*10 + nDigit;
  // #=hex digit
  else if (cMode == '#') m_nTemp = m_nTemp*16 + nHex;
  // e=long decimal digit
  else if (cMode == 'e') m_nLTemp = m_nLTemp*10 + nDigit;
  // a=angular measure
  else if (cMode == 'a') m_nLTemp = m_nLTemp*6 + nDigit;
  // t=Time - hhmm
  else if (cMode == 't') { m_nGpsTime = m_nTemp*10 + nDigit; m_nTemp = 0; }
  // m=Millisecs
  else if (cMode == 'm') { m_nGpsMsecs = m_nTemp*10 + nDigit; m_nTemp=0; }
  // l=Latitude - in minutes*10000
  else if (cMode == 'l') { if (nCh == 'N') m_nGpsLat = m_nLTemp; else m_nGpsLat = -m_nLTemp; m_nLTemp = 0; }
  // o=Longitude - in minutes*10000
  else if (cMode == 'o') { if (nCh == 'E') m_nGpsLong = m_nLTemp; else m_nGpsLong = -m_nLTemp; m_nLTemp = 0; }
  // j/k=Speed - in knots*100
  else if (cMode == 'j') { if (nCh != '.') { m_nTemp = m_nTemp*10 + nDigit; m_nGpsState--; } }
  else if (cMode == 'k') { m_nGpsKnots = m_nTemp*10 + nDigit; m_nTemp = 0; }
  // c=Course (Track) - in degrees*100
  else if (cMode == 'c') { m_nGpsCourse = m_nTemp*10 + nDigit; m_nTemp = 0; }
  // y=Date - ddmm
  else if (cMode == 'y') { m_nGpsDate = m_nTemp*10 + nDigit ; m_nTemp = 0; }
  // z=End of checksum range
  else if (cMode == 'z') { if (nCh == '*') m_bGpsChkEn = 0; else m_nGpsState = 0; }
  // x=End of checksum value
  else if (cMode == 'x') {
    m_nGpsChkVal = m_nTemp*16 + nHex; m_bGpsChkOk = (m_nGpsChkVal == m_nGpsChkSum); m_bGpsFix = 1; m_nGpsState = 0;
  }
  // If received character matches format string, or format is '?' - return
  else if ((cMode == nCh) || (cMode == '?')) { }
  //  Unexpected character; abort
  else m_nGpsState = 0;

  // If checksum enabled, add current byte
  if (m_bGpsChkEn) m_nGpsChkSum ^= nCh;
  if (cMode == '$') { m_bGpsChkEn = 1; m_nGpsChkSum = 0; }
}
					

ATtiny SPI slave interface

The GPS parser code records the decoded fields (such as GPS coordinates, date/time, ground speed, etc.) into individual sensor variables. We then transfer each of these variables into a set of SPI holding registers. The SPI slave code will monitor the external SPI interface and provide access to the SPI holding registers.

An additional SPI register is used for status -- it contains the VALID bit that we use to ensure we have coherent access to the registers.

Please refer to the ATtiny SPI slave page for more details.

In the case of this particular SPI slave implementation, the following registers have been defined to hold the various fields from the RMC NMEA message:

// Byte address definitions for 16-bit SPI registers
#define         REG_STATUS        0x00  // Indicate status of GPS data in registers
#define         REG_FIXED         0x02  // Fixed value (0x1234) for debug
#define         REG_GPS_TIME      0x04  // GPS: Time (hours*100 + minutes)
#define         REG_GPS_MSECS     0x06  // GPS: Time (seconds*1000 + milliseconds)
#define         REG_GPS_KNOTS     0x08  // GPS: Speed over ground
#define         REG_GPS_COURSE    0x0A  // GPS: Course over ground
#define         REG_GPS_DATE      0x0C  // GPS: Date (month*100 + days)
#define         REG_GPS_LAT_H     0x0E  // GPS: Latitude (upper word)
#define         REG_GPS_LAT_L     0x10  // GPS: Latitude (lower word)
#define         REG_GPS_LONG_H    0x12  // GPS: Longitude (upper word)
#define         REG_GPS_LONG_L    0x14  // GPS: Longitude (lower word)
#define         REG_GPS_CHECK     0x16  // GPS: Checksum expected and actual

#define         REGMAX            0x18  // Size of register array
#define         REGH              0
#define         REGL              1

// The SPI register array
volatile uint8_t m_anRegArray[REGMAX];				
				

Coherent SPI register access

A very important consideration here is that we must provide coherent access to these SPI registers. The arrival of an external SPI read command can happen at any time versus the GPS parser updating the holding registers. This means that we might be part-way through updating a GPS coordinate when a request arrives from the SPI interface to read the coordinate. If we didn't provide any special code to handle this coherency, then the result would be a corrupted GPS coordinate (ie. one or more bytes of the coordinate value would be old, while the remainder of the coordinate bytes would be new).

One easy way to implement coherent access to the registers is to provide a read/write VALID bit in an extra SPI status register. The GPS parsing code only transfers the contents of the GPS variables into the SPI holding registers when the VALID bit is clear (zero). After the GPS variables have been copied into the SPI holding registers, the VALID bit is set (one). Note that in the minimal GPS parser code above, I use the m_nGpsFix flag to signal that all of the GPS variables have finished updating.

The SPI master must be disciplines to ensure that it reads the state of the VALID bit before reading the other SPI holding registers. If the VALID bit was clear, then it must disregard (or not read) the content of the holding registers. If the VALID bit was set, then the SPI holding registers can be read coherently. After the SPI master is done with the reads, it then clears the VALID bit (typically by writing a 1 to the VALID register bit), which frees up the bridge to latch the next set of sensor variable data into the SPI holding registers.

SPI slave register coherency with GPS UART sensor data

The above diagram shows the operations that appear on both sides of the UART-to-SPI bridge. Registers highlighted in yellow depict when they are updated. On the right side, the GPS module is sending UART data bytes towards the ATtiny (which implements the bridge). For the purposes of this example, I am representing four GPS fields (eg. latitude, longitude, date and time) by A, B, C and D. The ATtiny parses the bytes of the GPS NMEA messages and saves them into the GPS sensor variables (represented by Va,Vb,Vc,Vd). Each time the last field has been received at the UART, the ATtiny checks to see if the VALID bit is clear (ie. free). If it is set (ie. latched data already exists), the new sensor field data is discarded. If the VALID bit is clear (ie. SPI registers are ready for new latched data), then all sensor fields are copied into the SPI holding registers (Sa, Sb, Sc. Sd).

After all of the SPI holding registers have been updated, the bridge then proceeds to set the VALID flag to indicate that new data exists and that it is completely written.

In parallel with the above, the SPI master is attempting to poll / read the SPI register containing the VALID flag. If it is set, it can then proceed to read the SPI holding registers (Sa, Sb, Sc, Sd). Finally, the SPI master indicates that it is done reading the latched data by clearing the VALID bit. By convention, this "write-1-to-clear" methodology involves defining a SPI register bit that, when written with a value of 1 causes it to be reset.

Code to Implement Coherent SPI Slave Register Access

The following code incorporates the above SPI slave register access methodology so that a number of GPS fields can be read by the SPI master (eg. Raspberry Pi). Note that additional code is required to complete the SPI slave as this is just a small section.

void loop() {
  // ...	
  int    nUartIn;
  
  // Determine if there is an incoming UART RX byte ready to be processed
  if (Serial.available()) {
    nUartIn = Serial.read();
    if (nUartIn != -1) {

      // Process the UART RX byte (nUartIn)

      // Reset the fix indicator
      // - This lets us detect if we have just completed the last byte
      //   of the NMEA string. This is used to ensure we have coherency
      //   with all the GPS variables when we copy them to the SPI regs.
      m_bGpsFix = 0;
      
      // Feed GPS data directly into parser
      // - This will update the m_nGps* variables
      // - On the last byte of the special NMEA sentence, we will set m_bGpsFix=1
      ParseGPS(nUartIn);

      // If we just completed parsing the special message, we expect the following
      // variables have been assigned:
      // - m_bGpsFix   = 1
      // - m_bGpsChkOk = 0/1
        
      // Only latch the new values into reg array when we have just completed updating all GPS variables
      if (m_bGpsFix) {

        #ifdef LED_FLASH_GPS_EN        
        SETPIN1_LED_H();
        #endif

        // Only update the I2C reg array when the regs are marked as ready for
        // new data (ie. invalid).
        if (m_bGpsValid == 0) {
          
          // Copy over all the GPS variables into the I2C reg array
          // This is coherent because we know we've finished an NMEA message
          // by the fact that m_bGpsFix=1 in this cycle.
           
          m_anRegArray[REG_GPS_TIME  +REGH] = (m_nGpsTime   & 0xFF00) >> 8;
          m_anRegArray[REG_GPS_TIME  +REGL] = (m_nGpsTime   & 0x00FF);
          m_anRegArray[REG_GPS_MSECS +REGH] = (m_nGpsMsecs  & 0xFF00) >> 8;
          m_anRegArray[REG_GPS_MSECS +REGL] = (m_nGpsMsecs  & 0x00FF);
          m_anRegArray[REG_GPS_KNOTS +REGH] = (m_nGpsKnots  & 0xFF00) >> 8;
          m_anRegArray[REG_GPS_KNOTS +REGL] = (m_nGpsKnots  & 0x00FF);
          m_anRegArray[REG_GPS_COURSE+REGH] = (m_nGpsCourse & 0xFF00) >> 8;
          m_anRegArray[REG_GPS_COURSE+REGL] = (m_nGpsCourse & 0x00FF);
          m_anRegArray[REG_GPS_DATE  +REGH] = (m_nGpsDate   & 0xFF00) >> 8;
          m_anRegArray[REG_GPS_DATE  +REGL] = (m_nGpsDate   & 0x00FF);
          m_anRegArray[REG_GPS_LAT_H +REGH] = (m_nGpsLat    & 0xFF000000)>>24;
          m_anRegArray[REG_GPS_LAT_H +REGL] = (m_nGpsLat    & 0x00FF0000)>>16;
          m_anRegArray[REG_GPS_LAT_L +REGH] = (m_nGpsLat    & 0x0000FF00)>>8;
          m_anRegArray[REG_GPS_LAT_L +REGL] = (m_nGpsLat    & 0x000000FF)>>0;
          m_anRegArray[REG_GPS_LONG_H+REGH] = (m_nGpsLong   & 0xFF000000)>>24;
          m_anRegArray[REG_GPS_LONG_H+REGL] = (m_nGpsLong   & 0x00FF0000)>>16;
          m_anRegArray[REG_GPS_LONG_L+REGH] = (m_nGpsLong   & 0x0000FF00)>>8;
          m_anRegArray[REG_GPS_LONG_L+REGL] = (m_nGpsLong   & 0x000000FF)>>0;      

          // For debug purposes, provide expected and actual checksum values
          m_anRegArray[REG_GPS_CHECK +REGH] = (m_nGpsChkVal & 0x00FF);
          m_anRegArray[REG_GPS_CHECK +REGL] = (m_nGpsChkSum & 0x00FF);

          // Indicate SPI register contains new value
          m_bGpsValid = true;

          // Update status register
          uint8_t nNewStatus = 0x00;
          nNewStatus |= (m_bGpsValid)?0x80:0x00;
          nNewStatus |= (m_bGpsFix)?0x40:0x00;
          nNewStatus |= (m_bGpsChkOk)?0x20:0x00;
          m_anRegArray[REG_STATUS    +REGH] = nNewStatus;           
          m_anRegArray[REG_STATUS    +REGL] = 0x00;
          
        } // m_bGpsValid
      } // m_bGpsFix
     
    } // nUartIn
  } // Serial.available()

}
					

 


Reader's Comments:

Please leave your comments or suggestions below!
2016-12-15Josep
 First of all, great job.

I'm working in the final project of my studies. That project consists in a satellite compass for vessels and it will be mounted with 3 gnss modules and a RPi 2B. The gnss modules outputs data by UART interface and I need put the 3 signals to the RPi and I know thats is not possible to do directly. After read your post I'm thinking about if is possible convert the UART to SPI (or I2C) and resend the data to the RPi SPI/I2C pins. Do you know how can I do that?

Thanks so much
 I do think it should be straightforward to do what you are thinking. My recommendation would be to consider using three GPS UART to I2C micros (each with a different I2C address) and then share these on a single RPI I2C bus which polls all three.

If you have any questions/troubles with the UART to I2C let me know!
2016-06-15Spacecoaster
 This is a great write-up. I found this article while looking for information on using a serial Ublox-7m based module on an I2C bus. Are you aware of any other resources where I might find code to use a Digispark to interface a serial GPS module to I2C?

I can probably manage to modify your example to use I2C instead of SPI if I have to. Thanks for the excellent resource.
 Hi --

Thanks! Good timing... I have just completed an I2C version! The alternate code provides the GPS information through I2C registers using a Digispark. It took me longer than expected because of the challenges faced in making the I2C reliable with the Raspberry Pi I2C clock stretching bug. The ATtiny I2C slave code I am ultimately using seems to be quite stable on I2C after tweaking the timing.

Note that I have updated the GPS parsing code above to handle the Course-over-Ground field that is optionally excluded by the uBlox when stationary.

I plan to post the I2C GPS with ATtiny article soon once I have written up some details regarding the I2C issues.

 


Leave a comment or suggestion for this page:

(Never Shown - Optional)
 

Visits!