Skip to content

feat: add GPS UART module with NMEA parsing and checksum validation #600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
12 changes: 12 additions & 0 deletions uart/gps_uart/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Tell CMake where to find the executable source file
add_executable(gps_uart gps_uart.c)

# Link to pico_stdlib (gpio, time, etc. functions)
target_link_libraries(gps_uart pico_stdlib hardware_uart)

# Enable usb output, disable uart output
pico_enable_stdio_usb(gps_uart 1)
pico_enable_stdio_uart(gps_uart 1)

# Create map/bin/hex/uf2 files
pico_add_extra_outputs(gps_uart)
191 changes: 191 additions & 0 deletions uart/gps_uart/gps_uart.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#include "hardware/uart.h"
#include "pico/stdlib.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

// uart id and tx/rx pins could vary according to the microcontroller used
// please refer to the datasheet
#define UART_ID uart1
#define BAUD_RATE 9600
#define UART_TX_PIN 4
#define UART_RX_PIN 5
Comment on lines +22 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@peterharperuk Is it worth putting these in the pico_binary_info thingy?

#define MAX_NMEA_LENGTH 256

typedef struct
{
float latitude;
float longitude;
bool is_valid;
} GPSData;


void uart_gps_init()
{
uart_init(UART_ID, BAUD_RATE);

gpio_set_function(UART_TX_PIN, GPIO_FUNC_UART);
gpio_set_function(UART_RX_PIN, GPIO_FUNC_UART);

uart_set_hw_flow(UART_ID, false, false);
uart_set_format(UART_ID, 8, 1, UART_PARITY_NONE);
uart_set_fifo_enabled(UART_ID, true);
}

bool validate_nmea_checksum(char *nmea_string)
{
int len = strlen(nmea_string);
printf("Checksum Validation - String Length: %d\n", len);
printf("Full NMEA String: %s", nmea_string);

if (len < 7)
{
printf("Invalid: Too short (< 7 chars)\n");
return false;
}

char *checksum_ptr = strchr(nmea_string, '*');
if (!checksum_ptr)
{
printf("Invalid: No checksum marker (*) found\n");
return false;
}

uint8_t calculated_checksum = 0;
for (char *p = nmea_string + 1; p < checksum_ptr; p++)
{
calculated_checksum ^= *p;
}

char hex_checksum[3];
snprintf(hex_checksum, sizeof(hex_checksum), "%02X", calculated_checksum);

printf("Calculated Checksum: %s\n", hex_checksum);
printf("Received Checksum: %s\n", checksum_ptr + 1);

bool is_valid = strncmp(hex_checksum, checksum_ptr + 1, 2) == 0;
printf("Checksum Validation: %s\n", is_valid ? "VALID" : "INVALID");

return is_valid;
}

void convert_nmea_to_decimal(char *nmea_coord, float *decimal_coord)
{
float degrees = atof(nmea_coord) / 100.0;
int int_degrees = (int)degrees;
float minutes = (degrees - int_degrees) * 100.0;

*decimal_coord = int_degrees + (minutes / 60.0);
}

bool parse_nmea_gps(char *nmea_string, GPSData *gps_data)
{
if (!validate_nmea_checksum(nmea_string))
{
printf("Invalid NMEA Checksum\n");
return false;
}

// Ignore all NMEA strings that are not of type GPRMC
if (strncmp(nmea_string, "$GPRMC", 6) != 0)
{
return false;
}

char *token;
char *tokens[12] = {0};
int token_count = 0;

// Tokenize the string
token = strtok(nmea_string, ",");
while (token != NULL && token_count < 12)
{
tokens[token_count++] = token;

token = strtok(NULL, ",");
}

// Check if we have enough tokens and data is valid
if (token_count >= 12 && strcmp(tokens[2], "A") == 0)
{
if (strlen(tokens[3]) > 0)
{
convert_nmea_to_decimal(tokens[3], &gps_data->latitude);
if (tokens[4][0] == 'S')
gps_data->latitude = -gps_data->latitude;
}

if (strlen(tokens[5]) > 0)
{
convert_nmea_to_decimal(tokens[5], &gps_data->longitude);
if (tokens[6][0] == 'W')
gps_data->longitude = -gps_data->longitude;
}

gps_data->is_valid = true;
return true;
}
}

void process_gps_data(GPSData *gps_data)
{
char nmea_buffer[MAX_NMEA_LENGTH];
int chars_read = 0;

while (uart_is_readable(UART_ID) && chars_read < MAX_NMEA_LENGTH - 1)
{
char received_char = uart_getc(UART_ID);
nmea_buffer[chars_read] = received_char;

// NMEA strings are terminated by a newline character
if ((int)received_char == 10)
{
nmea_buffer[chars_read + 1] = '\0';

if (parse_nmea_gps(nmea_buffer, gps_data))
{
printf("Valid GPS Data Received\n");
}

chars_read = 0;
break;
}
else
{
chars_read++;
}
}
}

int main()
{
stdio_init_all();

uart_gps_init();

// recommended cold start waiting time (could vary based on the gps module)
sleep_ms(7 * 60 * 1000);

GPSData gps_data = {0};

while (1)
{
process_gps_data(&gps_data);

if (gps_data.is_valid)
{
printf("GPS Location:\n");
printf("Latitude: %.6f\n", gps_data.latitude);
printf("Longitude: %.6f\n", gps_data.longitude);
gps_data.is_valid = false;
}

// sufficient waiting time required between each gps reading and the next one
sleep_ms(30 * 1000);
tight_loop_contents();
}

return 0;
}