Fun With Arduino, Global Navigation Satellite Systems (GNSS) and Teseo III

Table of contents

Fun With Arduino, Global Navigation Satellite Systems (GNSS) and Teseo III

Abstract

A GPS / Galileo / BeiDou / GLONASS receiver for Arduino using ST's Teseo III technology. This project covers how navigation systems work, and how to connect a GPS module, antenna, and extract timing, location, altitude and satellite information.


Introduction

I’ve never built anything GNSS or Global Positioning System (GPS)-related before, but decided to give it a go. This blog post documents what I discovered!

image

How Does it Work?

There are well over a hundred satellites in orbit, which transmit signals containing timing information and satellite position. The timing information comes from an ultra-precise clock inside each satellite. For the satellite position, imagine a sphere surrounding the planet. The position can be defined as an angle from one of the poles, and the angle around the planet (for instance around the equator), which meet up at the satellite location on that sphere.

The information is transmitted at a frequency of about 1.5 GHz, and, due to the distance of the satellite orbit, it takes about 10 msec to arrive on Earth.

The received signal is very weak, and would be impossible to pull out of the noise, so it is sent relatively slowly (50 bits per second), with each bit of data being encoded with a special (much faster) binary pattern before it’s transmitted. The GPS receiver tries to match up the pattern. When it does that, then, mathematically, the signal can be recovered from the noise; the pattern is known as a pseudo-random sequence (although it contains the word ‘random’, it’s not random in the normal sense; it looks random to a casual viewer, but in reality it has a pattern that can be exactly replicated by the receiver), and the matching effect is known as autocorrelation. Together, it is known as Spread Spectrum communication. It’s pretty amazing, because it’s invisible on a spectrum analyzer (since it’s below the noise floor!) until autocorrelation is achieved by the receiving system.

Another neat trick is that many satellites can transmit simultaneously, each with a different pseudo-random sequence, and the receiver can, in parallel, perform multiple matching operations, to achieve autocorrelation for each signal.

The receiver is designed to have the ability to time things too, but not to the same accuracy as the satellites! The receiver will locally timestamp the arrival time of each message, and then compare that local timestamp with the time encoded in the satellite signals. Once there are signals from at least four satellites (the more the better!), then there is enough information to use simultaneous equations, to solve for the location of the receiver.

Today, there are more global navigation systems than just GPS out there; there is also Galileo (European), BeiDou (Chinese) and GLONASS (Russian), all operating in a similar way. There are also more geographically-specific navigation systems too. A GNSS receiver (colloquially known as a GPS receiver) can receive information from several of the navigation systems, and therefore provide more accurate location information, by selecting based on received signal quality.

In order to make use of GNSS, nowadays a single chip can be used. The chip will typically receive the RF signals, down-convert to a much lower intermediate frequency (IF), perhaps zero-IF, then perform the correlation operation, and then firmware (either pre-programmed, or uploaded into the chip) will compute the receiver location. To simplify things, modules are available, with in-built crystal oscillator and pre-programmed firmware.

Building the Prototype

I searched for a GNSS module that was fairly recent, so that it would be sensitive and support lots of features. Eventually, I settled on a Lantronix part PNT-SG3FS that costs about £12, and supports all the major satellite navigation systems; GPS, Galileo, BeiDou and GLONASS. Internally, the module contains a modern Teseo III (product brief PDF) series chip from ST. I believe the module is physically and functionally identical to ST’s TESEO-LIV3F module, but I have not tried it.

image

The module was tiny! It had a footprint about the size of a sugar cube. Fortunately, the connection pads were spaced reasonably well (1.1mm pitch), so it wouldn’t be too difficult to solder it onto a printed circuit board.

image

After consulting the module documentation, I settled on a Taoglas antenna (PDF datasheet), which is a 1-inch square device, that needs a 70x70mm ground plane preferably. There were various options; I went with an antenna that is mounted on the reverse side of the board. It is stuck down with double-sided adhesive tape, and then the pin is soldered.

image

I used a copper-clad board since I had not designed a PCB yet. Ideally it should be double-sided. Mine was single-sided, so I put copper slug tape on the other side, and then patched both sides together with loads of holes and wire links soldered through them all. This part of the exercise was time-consuming and was not fun!

The Lantronix Teseo III module was tricky to patch wires onto. A PCB as mentioned would be far easier.

image

When powered up, the prototype immediately functioned, which was a surprise, because the board was not close to a window; it was several meters away. I had not expected that, given the construction method. The Teseo III module and the Taoglas antenna seem to function well together, but it would be interesting to try other antennas, and/or a low noise amplifier (LNA) one day.

Anyway, long story short, since the prototype worked, I went ahead and produced a KiCad PCB design, which broadly follows the prototype layout. The boards should arrive in a couple of weeks.

image

Arduino Connections

The Teseo module cannot work with 5V logic levels, so a 3.3V logic-level Arduino is required. I used an Arduino Uno R4 Minima , adapted for 3.3V operation. If you wish to do the same thing, the information is here:  Modifying the Arduino Uno R4: Making it 3.3V-Friendly  

The module operates from a 3.3V supply, but the PCB contains a voltage regulator, which needs a 5V supply, which could be taken from from the Arduino board if it has a connection for that (depends on the particular model of Arduino board that is used).

Module Pin J1 Pin on PCB Arduino Pin Description
2 7 D0/RX Transmit from GNSS (GPS_TX)
3 6 D1/TX Transmit from Arduino (GPS_RX)
- 3 5V 5V supply for GNSS board
- 1 GND GND

NMEA Sentences

When powered up, the module should emit a regular stream of text data over the serial UART. The data is known as NMEA sentences; they all begin with $, followed by two characters (often GP, signifying GPS), and then three characters indicating the record type. After that, there is a comma-separated list of parameters, usually ending with an asterisk and a two-digit checksum. Each line (record) in the serial stream has a termination that could be \r\n (i.e. 0x0d 0x0a, which is carriage-return and line-feed respectively).

$GPRMC,004609.000,A,5130.07200,N,00020.76010,W,0.2,0.0,040624,,,A*73
$GPGGA,004609.000,5130.07200,N,00020.76010,W,1,10,0.8,024.48,M,47.1,M,,*76
$GPVTG,0.0,T,,M,0.2,N,0.3,K,A*0C
$GNGSA,A,3,10,23,12,19,17,22,15,24,,,,,1.7,0.8,1.5*29
$GNGSA,A,3,73,71,,,,,,,,,,,1.7,0.8,1.5*24
$GPGSV,3,1,10,24,64,276,29,15,62,176,31,22,44,059,34,13,38,132,*76

In the example above, the first line shows a record called RMC that originated from the GPS system. The record names are cryptic, they are described in the NMEA specification (which is not free, but plenty of online information exists). All I have learned for now, is that the RMC record contains time/date/longitude/latitude information, the GGA record contains altitude, and the GSV record contains satellite information.

You could choose to ignore the checksums from the module, but obviously, if you really are navigating, say a ship, then it wouldn’t be wise to do that!

The checksum is computed as follows:

Set a variable called checksum to zero. Iterate across the NMEA sentence, ignoring the $ and the asterisk, and for each ASCII character, let’s call it c, calculate: checksum = checksum ^ c

Finally, convert the value in checksum into a two-character hexadecimal string, and append it after the asterisk.

Software

There are some pre-existing Arduino libraries, for instance, one called Adafruit_GPS; however, since I was in a learning phase, I wanted to parse the data from scratch so that I could get used to the NMEA sentences that the module would emit. I tested the code using an Arduino Uno R4 Minima board. The code assumes that the Arduino board has a hardware serial interface, known as Serial1. Many Arduino boards have this, but the older Arduino Uno Rev 3 and Arduino Nano (classic version) does not. If you want to use an Arduino Uno Rev 3 or Nano (classic version), then the library code needs to be improved, to be able to use SoftwareSerial capability.

My code isn’t bullet-proof, it will fall over if the data stream is corrupted. The code doesn’t verify checksums either. However, it’s simple-to-use. To use it, add the Teseo Library .ZIP file into the Arduino development environment (IDE) using the menu Sketch->Include Library->Add .ZIP file.

Once that’s done, you can open the example Arduino .ino file by clicking File->Examples->Teseo Library->TeseoTest and run it. The example code demonstrates how to create a GNSS object, and then call flush_data, get_data, and print_data functions.

If it’s successful, you’ll see a stream of information appear repeatedly in the Serial Monitor:

image

In the screenshot above, the ‘A’ status means the information is valid (‘V’ means it’s not! NMEA peculiarity : )).

The latitude and longitude are indicated in degrees North and East respectively; the example shows a longitude of -0.346 degrees East, which is +0.346 degrees West.

The altitude is interesting; the earth isn’t spherical, it bulges a little, i.e. it is slightly ellipsoidal, plus, on top of that, sea level depends on gravity, and gravity isn’t constant across the earth. Where the earth is denser, it attracts more water, and the mean sea level rises there. There is an internal table of heights inside the module, and the reported altitude takes that table into account, to try to provide an altitude value that is referenced off the internal table of mean sea levels at the reported location. It’s known as an orthometric height. If required, the difference between that and the simpler ellipsoidal model is the value reported as Geoid separation.

The satellite information contains a Source field; A value of 1 indicates a GPS satellite.

image

The PRN value is a unique identifier per satellite. The SNR value is between 0 and 99, and the higher the number, the better the signal strength. A value of 0 in the output means that no SNR value was reported by the module (I don’t know what that means other than perhaps that the satellite is not being used for the calculations).

Here’s a breakdown of how to use the code.

First, include the library:

#include <Teseo.h>

Create an object; for instance, call it gnss:

Teseo gnss;

Next, you can initialize it to 9600 baud (this is the default rate that the module uses):

gnss.init(9600);

To read data, two functions are called. The first one clears out the buffer, and the second retrieves fresh data from the device, parses it, and stores it.

gnss.flush_buffer();

gnss.get_data(PRINT_ENABLE);

Use PRINT_ENABLE if you wish to dump the raw NMEA sentences to the Arduino Serial Monitor, otherwise use PRINT_DISABLE.

By now, decoded data is stored inside the gnss object. If you just wish to print that decoded information, use:

gnss.print_data();

If you wish to retrieve specific bits of decoded information, you will have to access the internal structures defined in Teseo.h, since the library is still very basic!

For instance, to obtain the current hours and minutes:

hour = gnss.rmc.hour;

min = gnss.rmc.min;

Summary

This blog post examined how satellite navigation systems worked, and a project was developed to make use of a GNSS Teseo III module from ST / Lantronix with the Arduino.

A simple Arduino library was created (to see how to create a custom library, click here: Arduino Library Creation and Testing Workflow ), that decodes the stream of NMEA sentences arriving from the module, to obtain time, location, altitude and satellite information.

Based on the functioning prototype, a PCB design was created, and although that has not been tested so far, hopefully, it will soon.

Thanks for reading!

References

Project GitHub Page

Lantronix PNT Series Specifications and Resources

ST Teseo Software User Manual (PDF)

Taoglas CGGBP.24.4.A.02 Antenna Datasheet (PDF)

NMEA Wikipedia Page

Creating Arduino Libraries

Category : Project
  • action image on a Raspberry Pi:

    left terminal controls the Teseo. That's where I send command strings to /dev/ttyS0
    Right terminal shows results. Command used is cat -s /dev/ttyS0

    image

  • I've been thinking about this more, and realized there is already some precedent for locally determining timezone without a (remote, e.g. API) lookup. That's wrist-watches! (well, high-end ones anyway).

    I looked in online user manuals, to see if there's any hint about how they do it. This snippet from a a typical user manual was unclear (the whole user manual is pretty bad), but does confirm that the watch can self-determine it:

    image

    This note confirms that there's inaccuracy when you're near a boundary:

    image

    I can't conclude how the function is precisely implemented, but it must be either based on an internally stored vectors or polygons for seeing if the longitude/latitude falls within such regions, or maybe doing the same sort of thing that I was thinking of, which was a map with lookup of the nearest pixel. I don't know what would be more efficient for a wristwatch; more complicated calculations, or throwing more ROM at the problem (probably the latter!).

    This was useful, nice summary of the timezone offsets, and a token city name within each timezone. I might try to replicate that in the code!

    image

  • A few commands that allow interaction from the command line

    baud rate:

    sudo stty -F /dev/ttyS0 9600

    read:

    cat -s /dev/ttyS0

    send some commands:

    echo $'$PSTMGPSSUSPEND\r\n' > /dev/ttyS0
    echo $'$PSTMGPSRESTART\r\n' > /dev/ttyS0

    the combination doesn't work reliable yet, but good enough to see that commands work, and output is generated

  • The hardware works well on a Raspberry Pi too.
    The image below is generatd by attaching GPS RX to Pi TX (pin 8) and TX to Pi RX (pin 10). 
    Enabled UART with pico_config, rebooted, and then executed:

    cat /dev/ttyS0

    image

  • other sources, like these two, define GGA LAT as DD and LON as DDD. That's what I'd expect:

    First source:

    image
    (although this source has issues with symbol and example for LON Slight smile)

    The second source is more helpful:

    image

  • The GGA definition seems to be off in the ST spec:

    image

    Their example shows 3 digits for degrees for both values, not matching either of the definitions:

    image

    For other messages in the same spec, they usually define LAT as DD and LON as DDD
    That's consistent with the value I actually get for GGA out of the Teseo:

    $GPGGA,121143.250,5051.79934,N,00422.57865,E,0,00,99.0,107.45,M,0.0,M,,*65

    I'll try to find the formal NMEA specifications ...

  • I finally got rid of that loop trying to throw away 0xff after reading.

    Turns out that after you submit a command, i2c sends 0xffs until the reply is ready. And that's not predictable (at least: I was not able to predict that). It is dependent on the communication speed. It can easily fill 100s of characters. That's an issue with multi-line responses such as the GVS reply.

    Instead of trying to read the reply into the buffer single shot (or making the buffer silly big), I changed my code to read single characters, and discard them from the buffer, as long as there has been nothing but 0xffs.

        memset (buf, 0, BUFFSIZE);  // initialise buffer before reading
        bool gotData = false;
        uint8_t *bufptr = buf;
        do {
            i2c_read_blocking(I2C_PORT, I2C_ADDR, bufptr, 1, false);
            if (*bufptr != 0xff) {
                gotData = true;
                bufptr++;
            } else if (gotData) { // we are done
                *bufptr = 0;
                bufptr = buf + BUFFSIZE;
            }
        }
        while (bufptr - buf < BUFFSIZE);

    There is a risk introduced by this, that I will mitigate:

    If sending a command failed, the reply would be oxff forever. Resulting in an eternal loop. 

    I will set a parameter that tells the code how many times it can read a 0xff before giving up ....

    I've had up to 1261 0xff characters before a reply starts to flow, when using 100kbaud i2c. If I have to size the buffer for that, it's a waste of resources.
    Ignoring initial 0xffs, is a memory saver.

  • Once I start querying for more than one parameter, I get worse quality results.
    If I first ask for GPGLL, then GPRMC, I usually get wrong replies for the second request. I receive replies on the previous command.

    It takes between 2 (minimal) and 4 (often) retries before I get the expected reply.

    I can handle it in code, but wasn't expecting it after reading the ST appnote.

    Mitigation: I provided a retries parameter (default set to 0):

    bool teseo::ask_nmea(const nmea_rr& command, std::string& s, uint retries) {
        bool retval; // intentionally not initialised
        uint tries; // intentionally not initialised
        for (tries = 0; tries <= retries; tries++){
            write(command.first);
            read(s);
            retval = s.starts_with(command.second);
            if (retval) {
                break;
            }
        }
        return retval;
    }

    command.first is the sentence I send to the teseao, example - for GPRMC: "$PSTMNMEAREQUEST,40,0\n\r"
    command.second is the reply signature check, example - for GPRMC:"$GPRMC,"

    I always get a wrong command at least once, when I switch commands. I'm going to read the software document again, to see if I maybe have to ask both at the same time....

  • A side effect of my design approach,
    where the I2C read and write is not part of the teseo library, but is a "device dependent implementation that the user injects into the flow",
    is that I can feel less responsible for it Slight smile.

  • Once the code is all functioning, I'll write a flow chart based off your code, it will help others too. That way there will be flow-charts and example code for suggested or recommended (or at least working) methods for both I2C and UART. Your blogs are already all on the first page of Google results if someone types 'teseo gps code'!