// GPS NMEA parser with async UART DMA reception
use embassy_executor::Spawner;
use embassy_rp::bind_interrupts;
use embassy_rp::gpio::{Level, Output};
use embassy_rp::peripherals::UART0;
use embassy_rp::uart::{self, Config as UartConfig, InterruptHandler as UartInterruptHandler};
use embassy_rp::uart::{UartRx, UartTx};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
use embassy_time::{Duration, Timer};
use {defmt_rtt as _, panic_halt as _};
bind_interrupts!(struct Irqs {
UART0_IRQ => UartInterruptHandler<UART0>;
// -----------------------------------------------------------
// -----------------------------------------------------------
/// Signal to pass the latest GPS position from receiver to display task
static GPS_SIGNAL: Signal<CriticalSectionRawMutex, GpsPosition> = Signal::new();
// -----------------------------------------------------------
// -----------------------------------------------------------
#[derive(Clone, Copy, defmt::Format)]
const fn empty() -> Self {
// -----------------------------------------------------------
// -----------------------------------------------------------
fn verify_checksum(line: &[u8]) -> bool {
let mut checksum: u8 = 0;
let mut found_star = false;
let mut expected: u8 = 0;
for (i, &byte) in line.iter().enumerate() {
expected = hex_to_u8(line[i + 1], line[i + 2]);
found_star && checksum == expected
fn hex_to_u8(high: u8, low: u8) -> u8 {
b'0'..=b'9' => high - b'0',
b'A'..=b'F' => high - b'A' + 10,
b'a'..=b'f' => high - b'a' + 10,
b'0'..=b'9' => low - b'0',
b'A'..=b'F' => low - b'A' + 10,
b'a'..=b'f' => low - b'a' + 10,
fn split_fields<'a>(data: &'a [u8], fields: &mut [&'a [u8]; 20]) -> usize {
if data[i] == b',' || data[i] == b'*' {
fields[count] = &data[start..i];
fn parse_u8_2digit(high: u8, low: u8) -> u8 {
(high.wrapping_sub(b'0')) * 10 + (low.wrapping_sub(b'0'))
fn parse_u8_3digit(s: &[u8]) -> u16 {
(s[0].wrapping_sub(b'0') as u16) * 100
+ (s[1].wrapping_sub(b'0') as u16) * 10
+ (s[2].wrapping_sub(b'0') as u16)
fn parse_u8_field(s: &[u8]) -> u8 {
if b >= b'0' && b <= b'9' {
val = val.wrapping_mul(10).wrapping_add(b - b'0');
fn parse_nmea_float(s: &[u8]) -> f32 {
let mut integer_part: i32 = 0;
let mut decimal_part: i32 = 0;
let mut decimal_divisor: f32 = 1.0;
let mut in_decimal = false;
let mut negative = false;
} else if b >= b'0' && b <= b'9' {
decimal_part = decimal_part * 10 + (b - b'0') as i32;
integer_part = integer_part * 10 + (b - b'0') as i32;
let val = integer_part as f32 + decimal_part as f32 / decimal_divisor;
if negative { -val } else { val }
fn parse_gprmc(line: &[u8], pos: &mut GpsPosition) -> bool {
let mut fields = [&[] as &[u8]; 20];
let count = split_fields(line, &mut fields);
if fields[2].is_empty() || fields[2][0] != b'A' {
if fields[1].len() >= 6 {
pos.hours = parse_u8_2digit(fields[1][0], fields[1][1]);
pos.minutes = parse_u8_2digit(fields[1][2], fields[1][3]);
pos.seconds = parse_u8_2digit(fields[1][4], fields[1][5]);
if fields[3].len() >= 4 {
let degrees = parse_u8_2digit(fields[3][0], fields[3][1]) as f32;
let raw = parse_nmea_float(fields[3]);
let minutes_val = raw - (degrees * 100.0);
pos.latitude = degrees + minutes_val / 60.0;
if !fields[4].is_empty() && fields[4][0] == b'S' {
pos.latitude = -pos.latitude;
if fields[5].len() >= 5 {
let degrees = parse_u8_3digit(&fields[5][..3]) as f32;
let raw = parse_nmea_float(fields[5]);
let minutes_val = raw - (degrees * 100.0);
pos.longitude = degrees + minutes_val / 60.0;
if !fields[6].is_empty() && fields[6][0] == b'W' {
pos.longitude = -pos.longitude;
if !fields[7].is_empty() {
pos.speed_knots = parse_nmea_float(fields[7]);
fn parse_gpgga(line: &[u8], pos: &mut GpsPosition) -> bool {
let mut fields = [&[] as &[u8]; 20];
let count = split_fields(line, &mut fields);
if fields[6].is_empty() || fields[6][0] == b'0' {
if !fields[7].is_empty() {
pos.satellites = parse_u8_field(fields[7]);
if !fields[9].is_empty() {
pos.altitude_m = parse_nmea_float(fields[9]);
fn parse_nmea(line: &[u8], pos: &mut GpsPosition) -> bool {
if line.len() < 10 || line[0] != b'$' {
if !verify_checksum(line) {
let sentence_type = &line[1..6];
if sentence_type == b"GPRMC" {
} else if sentence_type == b"GPGGA" {
// -----------------------------------------------------------
// -----------------------------------------------------------
rx: &mut UartRx<'static, UART0, uart::Async>,
) -> Result<usize, uart::Error> {
rx.read(&mut byte).await?;
if pos < line_buf.len() {
if pos >= line_buf.len() {
// -----------------------------------------------------------
// Task 1: GPS Receiver (UART RX with DMA)
// -----------------------------------------------------------
#[embassy_executor::task]
async fn gps_receiver_task(mut rx: UartRx<'static, UART0, uart::Async>) {
let mut line_buf = [0u8; 128];
let mut position = GpsPosition::empty();
let mut sentence_count: u32 = 0;
defmt::info!("GPS receiver task started, waiting for NMEA data...");
// Read one complete NMEA sentence (line ending with '\n')
// During this await, the line_buf is exclusively borrowed.
// No other code can access it. Compiler-guaranteed.
match read_line(&mut rx, &mut line_buf).await {
if parse_nmea(&line_buf[..len], &mut position) {
// Send the updated position to the display task
GPS_SIGNAL.signal(position);
if sentence_count % 10 == 0 {
"Parsed {} sentences, fix={}",
defmt::warn!("UART read error: {:?}", e);
// Small delay before retrying on error
Timer::after_millis(100).await;
// -----------------------------------------------------------
// Task 2: GPS Display / Reporter
// -----------------------------------------------------------
#[embassy_executor::task]
async fn gps_display_task(mut fix_led: Output<'static>) {
defmt::info!("GPS display task started");
// Wait for a new position from the receiver task
let pos = GPS_SIGNAL.wait().await;
// Update fix indicator LED
defmt::info!("============ GPS Position ============");
"Time: {:02}:{:02}:{:02} UTC",
defmt::info!("Latitude: {}", pos.latitude);
defmt::info!("Longitude: {}", pos.longitude);
defmt::info!("Altitude: {} m", pos.altitude_m);
defmt::info!("Speed: {} knots", pos.speed_knots);
defmt::info!("Satellites: {}", pos.satellites);
defmt::info!("======================================");
defmt::info!("GPS: waiting for fix... ({} satellites)", pos.satellites);
// Blink the LED to indicate searching
Timer::after_millis(200).await;
// -----------------------------------------------------------
// -----------------------------------------------------------
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
// Configure UART0 for GPS at 9600 baud
let mut uart_config = UartConfig::default();
uart_config.baudrate = 9600;
let uart = uart::Uart::new(
p.PIN_0, // TX (Pico TX -> GPS RX)
p.PIN_1, // RX (Pico RX <- GPS TX)
p.DMA_CH0, // TX DMA channel
p.DMA_CH1, // RX DMA channel
// Split UART into TX and RX halves
// TX is unused in this project, but we take it to satisfy ownership
let (_tx, rx) = uart.split();
// Fix indicator LED on GP15
let fix_led = Output::new(p.PIN_15, Level::Low);
defmt::info!("GPS parser starting...");
defmt::info!("UART0: TX=GP0, RX=GP1, 9600 baud");
defmt::info!("Waiting for NMEA sentences from NEO-6M...");
spawner.spawn(gps_receiver_task(rx)).unwrap();
spawner.spawn(gps_display_task(fix_led)).unwrap();
Timer::after_secs(3600).await;
Comments