GPS coordinates directly from the NMEA page with ESP32 and PlataformIO

In this article I will demonstrate how to collect GPS coordinates directly from the page NMEA (National Marine Electronics Association). This format is a standard used for communicating navigation and position data from GPS systems.

The data sent by the GPS to the serial port usually consists of several sentences (text strings) in NMEA format, with information about the position, time, speed, direction and other parameters. Each NMEA sentence begins with a character “$” and is followed by information that is separated by commas.

Here is an example of a typical NMEA sentence sent by a GPS module:

$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47

With the hardware configured, this simple code and a GPS module connected to the ESP32, you will see in the terminal the same content shown in the photo above:

void loop() {
  while (Serial2.available()) {
    char c = Serial2.read();
    Serial.print(c);
  }
}

In this tutorial, we will create a Task in FreeRTOS that performs the continuous reading of selected data on this page, allowing its use in any application.

For this, we will use an ESP32 S3 WROOM -1 development module and a Ublox Neo M7 GPS module. However, it works with any ESP32 and GPS module:

  • GPS VCC3.3V of ESP32-S3
  • GPS GNDGND of ESP32-S3
  • GPS TXRX2 (GPIO16) of ESP32-S3
  • GPS RXTX2 (GPIO17) of ESP32-S3

Connecting ESP to Note and Creating the Project in VS Code

After connecting the ESP32 to GPS, you can now connect it to your computer using a cable USB.

📌 Creating the Project in VS Code with PlatformIO

1️⃣ Open VS Code and go to the PlatformIO.
2️⃣ Click on Create New Project.
3️⃣ During setup, select:

  • ESP Model you are using.
  • Framework: Arduino.

If you are following this tutorial and using the same ESP model, your file platformio.ini it will look like this:

[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
upload_speed = 921600

As mentioned above, the goal of this project is to receive NMEA sentences from a GPS receiver and extract the latitude, longitude, altitude and other relevant data. The coordinates are stored in a data structure and constantly updated, with the possibility of using them in other functions of the system.

Project Structure

  1. Collection of GPS NMEA sentences.
  2. Processing each sentence to extract data such as latitude, longitude, speed, etc.
  3. Validation of sentences using checksum verification.
  4. Using FreeRTOS to ensure that data collection is done continuously, without blocking code execution.

1. Initially we will declare the functions that will be used in the project.

#include <Arduino.h>

#define NUM_SENTENCAS 3

// --- Function Declarations ---

String seqRetData(String *SEQ, uint8_t pos);      // Returns the isolated data from the sequence based on the requested position
bool gpsSetData();                                // GPS Sets GPS data with processed sentences 
void gpsTask(void *pvParameters);                 // Task function to continuously collect GPS sentences
bool nmea0183_checksum(const char *nmea_data);    // Verifies if the checksum of an NMEA0183 sentence is correct

// -----------------------------
Expand

2. Declaring Variables and Structures

Next, we declare a structure Datagps which will store the collected data, such as latitude, longitude, number of satellites, and other GPS parameters. Below is the definition of this structure:

// --- GPS Data Structure ---
typedef struct
{
  float     lat;  
  float     lon; 
  uint8_t   fix; 
  uint8_t   sat; 
  float     hdop;
  float     alt; 
  float     vel; 
  float     bea;  
  char      hor[7];
  char      date[7]; 
  char      isfix[2];
  bool      isnew = false;   
  bool      isvalid = false; 
}  Datagps;

3. Global Variables

Here we have variables that store the data of the GPS sentences and the structure Datagps which keeps the data updated. Note that it is in the “sentence” variable that we declare which sentences on the NMEA page we are going to extract the information from.

// --- Global Variables ---

Datagps gps;  // GPS Data

String sentenca[NUM_SENTENCAS] = {"$GPGGA", "$GPVTG", "$GPRMC"};
String sentData[NUM_SENTENCAS];                               // Data for each sentence
bool sentencaValida[NUM_SENTENCAS] = {false, false, false};   // Flag to validate if the sentence was received and is correct

// --------------------------------------------------------------

4. Initial Setup Configuration do Setup()

The function setup() is responsible for initializing the system, configuring serial communication and creating the task FreeRTOS to continuously read GPS sentences.

void setup() {
  Serial.begin(115200);
  delay(100);
  Serial2.begin(9600, SERIAL_8N1, 16, 17);
  delay(100);
  Serial.println("NMEA GPS Sentence Example v1.0");  

  xTaskCreate(gpsTask, "GPS Task", 2048, NULL, 1, NULL);   // Adjust stack size as needed

  // Set to get new data
  gps.isnew = false;
  gps.isvalid = false;
}

Here, we configure serial communication for both the ESP32 debug and the GPS receiver connected to the Serial2 pin.

5. Main Loop

Inside the function loop(), we call the function gpsSetData() which is responsible for checking for new data and storing it in the structure Datagps.

void loop() {

  // Get GPS data from NMEA sentences
  gpsSetData();
  delay(1000);

}

6. FreeRTOS Task

The function gpsTask() is a task created by FreeRTOS to continuously collect GPS data without blocking the main program flow. In it, we monitor the serial communication, read and isolate the data sent by the GPS, and check if the received sentence is valid using the function checksum.

Remembering that at the beginning of the code we selected the sentences that we want to isolate on the NMEA page by declaring the following variable:

String sentenca[NUM_SENTENCAS] = {"$GPGGA", "$GPVTG", "$GPRMC"};

FreeRtos Task Function:

// --- Task function to continuously collect GPS sentences ---
void gpsTask(void *pvParameters) {
  char buffer[512];             // Buffer for serial reading
  int bufferIndex = 0;          // Buffer index
  String tempBuffer = "";       // Accumulator for serial data
  String currentSentence = "";  // Current sentence being read


  // Initialize with sentences not validated
  for (int i = 0; i < 3; i++) {
    sentencaValida[i] = false;
  }

  while (true) {
    // If there is data available on the serial
    if (Serial2.available() > 0) {
      // Continuous reading of serial data
      while (Serial2.available()) {
        char incomingByte = Serial2.read();  // Reads the next byte
        tempBuffer += incomingByte;   // Accumulates the read bytes
        // If we find the end of a sentence, like '\n'
        if (incomingByte == '\n') {          
          // Serial.println(tempBuffer);
          // For each requested sentence, check if it was found in the buffer
          for (int i = 0; i < 3; i++) {
            // Check if the sentence was found
            if (tempBuffer.startsWith(sentenca[i]) && !sentencaValida[i]) {
              // Stores the validated sentence
              if (nmea0183_checksum(tempBuffer.c_str())) {
                sentData[i] = tempBuffer;  // Stores the complete sentence
                sentencaValida[i] = true;  // Marks as valid
                Serial.print("Sentença " + sentenca[i] + " válida recebida: " + tempBuffer);
              } else {
                Serial.println("Erro de checksum na sentença: " + sentenca[i]);
              }
            }
          }
          tempBuffer = "";  // Clears the buffer after processing
        }
      }
    }
    
    // Check if all sentences were received
    bool allValid = true;
    for (int i = 0; i < 3; i++) {
      if (!sentencaValida[i]) {
        allValid = false;
        break;
      }
    }

    // If all sentences are valid, restart reading
    if (allValid) {
      // Serial.println("Todas as sentenças válidas foram recebidas!");
      gps.isnew = true;
      // Reset variables if necessary
      for (int i = 0; i < 3; i++) {
        sentencaValida[i] = false;
      }
    }
    
    vTaskDelay(100 / portTICK_PERIOD_MS); // Delay to avoid processor overload
  }
} // --- END Task function to continuously collect GPS sentences ---

Explanation of Logic

  • The buffer stores the received GPS data.
  • Each time a sentence ends (detected by '\n'), it is validated by checksum.
  • If the sentence is valid, it is stored in the variable sentData.

7. Checksum verification, validity of the collected sentence

Before processing any data, we ensure that the received sentence is valid by checking the checksum. The function nmea0183_checksum() calculates the checksum and compares it with the value sent by the sentence to ensure data integrity.

// --- Verifies if the checksum of an NMEA0183 sentence is correct ---
bool nmea0183_checksum(const char *nmea_data) {
  uint8_t crc = 0;
  
  // Ignores the '$' at the beginning and calculates the checksum
  int i = 1;
  for (i = 1; nmea_data[i] != '*' && nmea_data[i] != '\0'; i++) {
    crc ^= (uint8_t)nmea_data[i];
  }

  // Gets the checksum value of the sentence after the '*' character
  if (nmea_data[i] != '*') {return false;}
  int checksum = strtol(&nmea_data[i + 1], NULL, 16);

  // Verifies if the calculated checksum matches the sentence checksum
  return crc == checksum;
} // --- END Verifies if the checksum of an NMEA0183 sentence is correct ---

8. Processing of Received Data

The function gpsSetData() processes the received sentences, extracting data on latitude, longitude, altitude, among others. The function seqRetData() is used to isolate the desired fields of each sentence.

/ --- Sets GPS data with processed sentences ---
bool gpsSetData()
{
  // Verify if new GPS data is available
  if (!gps.isnew) {return false;}
  gps.isnew = false;

  String dat;
  float val;

  /// GGA Sequence-------------------------------------

  if (sentData[0] == "") {return false;}

  // Checks if it is fixed (6th position)
  dat = seqRetData(&sentData[0], 6);
  gps.fix =  dat.toInt();
  // Gets Latitude
  dat = seqRetData(&sentData[0], 2);
  val = dat.toFloat();
  dat = seqRetData(&sentData[0], 3);
  if(dat=="S") {val = val*(-1);}
  gps.lat = val*0.01;
  
  // Gets Longitude
  dat = seqRetData(&sentData[0], 4);
  val = dat.toFloat();
  dat = seqRetData(&sentData[0], 5);
  if(dat=="W") {val = val*(-1);}
  gps.lon = val*0.01;  
  
  // Gets Altitude
  dat = seqRetData(&sentData[0], 9);
  gps.alt = dat.toFloat();

  // Gets number of satellites
  dat = seqRetData(&sentData[0], 7);
  gps.sat = dat.toInt();

  // Gets HDOP
  dat = seqRetData(&sentData[0], 8);
  gps.hdop = dat.toFloat();

   // VTG Sequence-------------------------------------

  // Gets speed in km/h
  dat = seqRetData(&sentData[1], 7);
  gps.vel = dat.toFloat(); 

  // RMC Sequence-------------------------------------

  // Gets UTC time
  dat = seqRetData(&sentData[2], 1); 
  dat.toCharArray(gps.hor, sizeof(gps.hor));

   // Gets date
  dat = seqRetData(&sentData[2], 9);
  dat.toCharArray(gps.date, sizeof(gps.date));

  // Gets if it is fixed
  gps.isfix[0] = (seqRetData(&sentData[2], 2) == "A") ? 'S' : 'N';

  // Gets bearing (direction in degrees)
  dat = seqRetData(&sentData[2], 8);
  gps.bea = dat.toFloat();

  // Verifies if the coordinate is valid based on fix, HDOP, and number of satellites
  if (gps.isfix[0] == 'S' && gps.hdop <= 3 && gps.sat >= 4) {gps.isvalid = true;} else {gps.isvalid = false;}

  // Prints collected data
  Serial.println("------------------------------");
  Serial.print("Lat: ");
  Serial.print(gps.lat,6);
  Serial.print("    Lon: ");
  Serial.println(gps.lon,6);
  Serial.print("Alt: ");
  Serial.println(gps.alt,1);
  Serial.print("Fix: ");
  Serial.println(gps.fix);
  Serial.print("Sat: ");
  Serial.println(gps.sat);
  Serial.print("Hdop: ");
  Serial.println(gps.hdop);  
  Serial.print("isFix: ");
  Serial.println(gps.isfix);
  Serial.print("isValid: ");
  Serial.println(gps.isvalid ? 'S' : 'N');
  Serial.print("Bea: ");
  Serial.println(gps.bea);
  Serial.print("Vel: ");
  Serial.println(gps.vel);
  Serial.print("Hor: ");
  Serial.println(gps.hor);
  Serial.print("Date: ");
  Serial.println(gps.date);

  Serial.println("------------------------------");

  return true;  
} // --- END Sets GPS data with processed sentences ---

Here, we check and extract data from sentences, performing coordinate and value conversions.

9. Function to isolate data from a sentence

The function seqRetData() is responsible for isolating data from a NMEA sentence based on the position of the desired data.

// --- Returns the isolated data from the sequence based on the requested position ---
String seqRetData(String *SEQ, uint8_t pos)
{
  String res = "", str = *SEQ;
  uint8_t i, ini = 0;
  uint8_t fim = str.length();

   // Counts the commas in the sequence until the desired position is found
  for (i = 0;i < pos;i++)
  {
    ini = str.indexOf(',', ini+1);
  }
  // Positioned, isolates the byte
  ini++; // Advances one position to get the data
  while (ini <= fim)
  {
    if (str[ini] == ',') {break;}
    res += str[ini];
    ini++;
  }
  return res;
} // --- END Returns the isolated data from the sequence based on the requested position ---

This function iterates through the sentence, counting commas to find the desired value in the correct position.

10. Conclusion

This project uses the protocol NMEA0183 to collect GPS data, process it and validate it in real time using FreeRTOS. With the structure Datagps, we can store data in an organized way and access it for other functions.

You can expand this project to store the coordinates on an SD card, send them via Bluetooth, or even display them on a display.

Leave a Comment

Your email address will not be published. Required fields are marked *

en_US
Scroll up