Coordenadas GPS direto da página NMEA com ESP32 e PlataformIO

Neste artigo vou demonstrar como coletar coordenadas de GPS direto da página NMEA (National Marine Electronics Association). Esse formato é um padrão utilizado para a comunicação de dados de navegação e posição de sistemas GPS.

Os dados enviados pelo GPS para a porta serial geralmente consistem em várias sentenças (strings de texto) no formato NMEA, com informações sobre a posição, horário, velocidade, direção e outros parâmetros. Cada sentença NMEA começa com um caractere “$” e é seguida por informações que são separadas por vírgulas.

Aqui está um exemplo de uma sentença NMEA típica enviada por um módulo GPS:

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

Com o hardware configurado, este simples código e um módulo GPS conectado ao ESP32, você verá no terminal o mesmo conteúdo mostrado na foto acima:

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

Neste tutorial, vamos criar uma Task em FreeRTOS que realiza a leitura contínua de dados selecionados nesta página, permitindo seu uso em qualquer aplicação.

Para isso, vamos utilizar um módulo de desenvolvimento ESP32 S3 WROOM -1 e um módulo GPS da Ublox Neo M7. No entanto funciona com qualquer ESP32 e módulo de GPS:

  • VCC do GPS3.3V do ESP32-S3
  • GND do GPSGND do ESP32-S3
  • TX do GPSRX2 (GPIO16) do ESP32-S3
  • RX do GPSTX2 (GPIO17) do ESP32-S3

Conectando o ESP ao Note e Criando o Projeto no VS Code

Após conectar o ESP32 ao GPS, você pode agora conectá-lo ao seu computador usando um cabo USB.

📌 Criando o Projeto no VS Code com PlatformIO

1️⃣ Abra o VS Code e vá até o PlatformIO.
2️⃣ Clique em Criar Novo Projeto.
3️⃣ Durante a configuração, selecione:

  • Modelo do ESP que está utilizando.
  • Framework: Arduino.

Se estiver seguindo este tutorial e usando o mesmo modelo de ESP, seu arquivo platformio.ini ficará assim:

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

Conforme mencionado acima o objetivo deste projeto é receber as sentenças NMEA de um receptor GPS e extrair as coordenadas de latitude, longitude, altitude e outros dados relevantes. As coordenadas são armazenadas em uma estrutura de dados e constantemente atualizadas, com a possibilidade de usá-las em outras funções do sistema.

Estrutura do Projeto

  1. Coleta das sentenças GPS NMEA.
  2. Processamento de cada sentença para extrair dados como latitude, longitude, velocidade, etc.
  3. Validação das sentenças usando a verificação de checksum.
  4. Uso de FreeRTOS para garantir que a coleta de dados seja feita continuamente, sem bloquear a execução do código.

1. Inicialmente vamos declarar as funções que serão utilizadas no projeto

#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. Declaração de Variáveis e Estruturas

Na sequência, declaramos uma estrutura Datagps que armazenará os dados coletados, como latitude, longitude, número de satélites, e outros parâmetros do GPS. Abaixo está a definição dessa estrutura:

// --- 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. Variáveis Globais

Aqui, temos variáveis que armazenam os dados das sentenças GPS e da estrutura Datagps que mantém os dados atualizados. Observe que é na variável “sentenca” que declaramos de quais sentenças da página NMEA vamos retirar as informações.

// --- 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. Configuração Inicial do Setup()

A função setup() é responsável pela inicialização do sistema, configuração da comunicação serial e pela criação da tarefa FreeRTOS para ler continuamente as sentenças GPS.

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;
}

Aqui, configuramos a comunicação serial tanto para o debug do ESP32 quanto para o receptor GPS conectado ao pino Serial2.

5. Loop Principal

Dentro da função loop(), chamamos a função gpsSetData() que é responsável por verificar se há novos dados e armazená-los na estrutura Datagps.

void loop() {

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

}

6. Tarefa FreeRTOS

A função gpsTask() é uma tarefa criada pelo FreeRTOS para continuamente coletar os dados do GPS sem bloquear o fluxo principal do programa. Nela, monitoramos a comunicação serial, lemos e isolamos os dados enviados pelo GPS, e verificamos se a sentença recebida é válida utilizando a função de checksum.

Lembrando que lá no início do código selecionamos as sentenças que queremos isolar na página NMEA através da declaração da seguinte variável:

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

Função da Task FreeRtos:

// --- 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 ---

Explicação da Lógica

  • O buffer armazena os dados do GPS recebidos.
  • Cada vez que uma sentença termina (detectada por '\n'), ela é validada pelo checksum.
  • Se a sentença for válida, ela é armazenada na variável sentData.

7. Verificação de Checksum, validade da sentença coletada

Antes de processar qualquer dado, garantimos que a sentença recebida é válida verificando o checksum. A função nmea0183_checksum() calcula o checksum e o compara com o valor enviado pela sentença para garantir a integridade dos dados.

// --- 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. Processamento de Dados Recebidos

A função gpsSetData() processa as sentenças recebidas, extraindo dados de latitude, longitude, altitude, entre outros. A função seqRetData() é utilizada para isolar os campos desejados de cada sentença.

/ --- 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 ---

Aqui, verificamos e extraímos os dados das sentenças, realizando conversões de coordenadas e valores.

9. Função para isolar os dados de uma sentença

A função seqRetData() é responsável por isolar os dados de uma sentença NMEA com base na posição do dado desejado.

// --- 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 ---

Esta função percorre a sentença, contando as vírgulas para encontrar o valor desejado na posição correta.

10. Conclusão

Este projeto utiliza o protocolo NMEA0183 para coletar dados GPS, processá-los e validá-los em tempo real usando FreeRTOS. Com a estrutura Datagps, conseguimos armazenar os dados de forma organizada e acessá-los para outras funcionalidades.

Você pode expandir esse projeto para armazenar as coordenadas em um cartão SD, enviá-las por Bluetooth ou até exibi-las em um display.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

pt_BR
Rolar para cima