Popis konstrukce jednoduché navigace s RPi Pico, Waveshare L76K GPS modulem a LCD displejem ST7920 (SPI sběrnice). Verze 0.11.

Navigace verze 0.11 prototyp na nepájivém poli

IMG 20251030 181425

Na nepájivém poli mě navigace zlobila. Občas se mi data z modulu L76K kazila, pravděpodobně kvůli problémům s kontakty. Proto jsem si spájel prototyp na desce universálního plošného spoje a umístil ho pod displej. Nyní navigace šlape jako hodinky.

Finální hardwarová verze svrchu

IMG 20251030 231838

Finální hardwarová verze zespodu

IMG 20251030 231908

Programovací techniky

Multiprocesing a mutexy. Kruhový seznam. Získání dat ze sériové linky.

Sběr dat z modulu L76K

GNSS modul L76K je jádrem celé navigace. Data získaná z navigačních satelitů GPS posílá po sériové lince asynchronním způsobem. Dat jsou posílána ve formě vět protokolem NMEA 0183. Každá věta začíná znakem $ a končí znakem *, za nímž jsou dva znaky kontrolního součtu a znaky \r a \n. Data ve větě jsou oddělena čárkou ,. Popis dat je dán normou NMEA 0183 a můžeme jej získat třeba z datasheetu výrobce Quectel_L76K_GNSS_Protocol_Specification_V1.1.pdf

Konkrétní věta vypadá třeba takto:

$GNGGA,181117.000,5020.93040,N,01555.34639,E,1,11,1.1,257.3,M,43.9,M,,*46\r\n
  • začátek Znak $ označuje začátek věty.

  • systém satelitů Písmena GN znamenají systém satelitů (GN je mix všech satelitů, GP je americký Global Positioning System, GL je ruský GLONASS a BD je čínský navigační systém BeiDou).

  • typ věty Písmena GGA je typ věty, v tomto případě je to jsou to fixní data globálního pozičního systému (Global Posiotioning System Fix Data).

  • data 181117.000 je aktuální světový čas (UTC). 18 jsou hodiny, 11 jsou minuty, 17.000 jsou sekundy.

  • data 5020.93040 je zeměpisná šířka (kde se nacházíme), 50 jsou stupně 20.93040 jsou minuty.

  • data Písmeno N znamená, že se nacházíme severně od rovníku, písmeno S by označovalo jižně od rovníku.

  • data 01555.34639 je zeměpisná délka, 015 jsou stupně a 55.234639 jsou minuty.

  • data Písmeno E znamená, že se nacházíme východně od nultého poledníku. Pokud by tam bylo W, tak se nacházíme západně od nultého poledníku.

  • data 1 určuje kvalitu informací, znamená konkrétně platná data. Místo toho tam může být 0, pak jsou data neplatná, nebo 2 což je rozdílový režim nebo 6, kdy jsou data odhadnuta.

  • data 11 znamená aktuální počet rádiově viditelných satelitů.

  • data 1.1 je horizontální fluktuace dat.

  • data 257.3 je výška nad mořem. Toto pole u neplatných dat bude prázdné.

  • data M znamená, že výška nad mořem je měřena v metrech.

  • data 43.9 je rozdíl mezi ideálním geoidem a výškou na mořem.

  • data M znamená, že předchozí hodnota je v metrech. (U neplatných dat bude prázdná).

  • data dvě čárky za sebou znamenají, že pole mezi nimi je prázdné

  • konec věty * je konec věty.

  • kontrolní součet Znaky 46 jsou kontrolním součtem všech znaků mezi $ a *. Počítá se jako exkluzivní NEBO (XOR).

  • konec znaky \r\n (nebudeme je potřebovat a proto je zahodíme).

Celá NMEA věta nepřekročí 127 znaků.

Typ věty, které nám modul L76K může poslat:

  • RMC doporučená minimální GNSS data (Recommended Minimum Specific GNSS Data.)

  • GGA fixní data GPS (Global Positioning System Fix Data.)

  • GSV viditelné satelity (GNSS Satellites in View.)

  • GSA GNSS fluktuace a aktivní satelity (GNSS DOP and Active Satellites.)

  • VTG azimut pohybu a rychlost pohybu (Course Over Ground & Ground Speed.)

  • GLL pozice — zeměpisná šířka a délka (Geographic Position – Latitude/Longitude.)

  • TXT textová informace o zařízení

  • ZDA světový čas (Time UTC) — zařízení L76K neumí určit lokální čas (nezná časouvou zónu v místě, kde se nachází).

Modul sype data po sériové lince do Pica neustále a asynchronně rychlostí 9600 baudů. To je v defaultním nastavení. V této verzi se podařilo nastavit rychlost sériové linky na 115200 baudů. Proto pro sběr dat ze sériové linky využijeme druhé jádro procesoru, které bude číst sériovou linku. Při čtení musíme rozpoznat začátek a konec NMEA věty a pokud máme větu načtenou ověříme její kontrolní součet. Jestliže nám kontrolní součet sedí, tak větu uložíme do buňky kruhového seznamu (nebo kruhové vyrovnávací paměti), označíme ji jako platnou a nezpracovanou, posuneme se v kruhovém seznamu dále a čteme dál. Tím se bude zabývat výhradně core1. První jádro core0 máme osvobozeno od časově náročného sběru dat a na něm se budeme věnovat zpracování sebraných dat a zobrazením na displeji.

V jazyce C to vypadá takto:

main.c
#define UBUF_LEN 9600+4
#define KBDATA_LEN 128
#define POCET_VET 75

uint8_t uart_buf[UBUF_LEN];

typedef struct kb {
    struct kb*          next;
    char*               data;
    int                 len;
    volatile bool       valid;
    volatile bool       zpracovano;
    mutex_t             mtx;
} KRUHOVY_BUFFER;

KRUHOVY_BUFFER kbdata[POCET_VET];

// nastavení pro sériovou linku
#define UART_ID         uart0
#define BAUD_RATE       9600
#define DATA_BITS       8
#define STOP_BITS       1
#define PARITY          UART_PARITY_NONE
#define UART_TX_PIN     0
#define UART_RX_PIN     1

// sběr dat ze sériové linky (běží na core1)
void seriova_linka( void )
{
int i = 0;     // pořadí znaku ve bufferu
uint8_t ch;    // znak ze sériové linky
KRUHOVY_BUFFER* kbptr = &kbdata[0]; // ukazatel na kruhovou vyrovnávací paměť
char buf[KBDATA_LEN]; // pomocný buffer
bool zacatek = false; // máme začátek věty
bool konec = false;   // máme konec věty

    kruhovy_buffer_init();

    while( true ) {
        // tady se sbírají data a ukládají do vyrovnávací paměti
        kbptr = kbptr->next;
    }
}

int main( void )
{
    // inicializace zařízení
    seriova_linka_init();
    // spouštíme sběr dat ze sériové linky na core1
    multicore_launch_core1(seriova_linka);

    while( true ) {
        // zpracování dat a zobrazení na displeji v pravidelných intervalech
    }
}

Kruhový seznam

Do kruhového seznamu (nebo kruhového bufferu) se ukládají ověřené NMEA věty, které si potom core0 zpracuje. Paměť je alokována staticky, seznam má místo pro 75 vět, buffer zabírá 9600 bytů.

kruhovy buffer

Inicializace kruhového bufferu
#define UBUF_LEN 9600+4
#define KBDATA_LEN 128
#define POCET_VET 75

uint8_t uart_buf[UBUF_LEN];

typedef struct kb {
    struct kb*          next;       // ukazatel na další kb
    char*               data;       // ukazatel na paměť pro NMEA větu
    int                 len;        // skutečná délka NMEA věty
    volatile bool       valid;      // true, pokud je věta platná a je možné ji zpracovat
    volatile bool       zpracovano; // true, pokud je věta zpracovaná a je možné ji přepsat
    mutex_t             mtx;        // mutex pro zamykání záznamu
} KRUHOVY_BUFFER;

KRUHOVY_BUFFER kbdata[POCET_VET];


// inicializace kruhového bufferu
void kruhovy_buffer_init(void)
{
    for( int j = 0; j<POCET_VET; j++)  {
        kbdata[j].valid      = false;
        kbdata[j].zpracovano = false;
        kbdata[j].data       = &uart_buf[j*KBDATA_LEN];
        kbdata[j].next       = &kbdata[j+1];
        kbdata[j].len        = 0;
        mutex_init(&kbdata[j].mtx);
    }
    kbdata[POCET_VET-1].next = &kbdata[0]; // poslední ukazuje na první a kruh je uzavřen
}

Příznak volatile bool valid;: true slouží příjemci věty k tomu, že nemusí počítat kontrolní součet (checksum) a může větu zpracovat. Příznak volatile bool zpracovano;: true je signálem pro zapisovatele dat (funkci seriova_linka()), že předchozí věta byla ve funkci main() zpracována a místo v kruhovém bufferu je možné přepsat. Pokud main() nestihne včas větu zpracovat, tak se na to nebere ohled a zapisovatel ji přepíše novou větou.

Mutexy (zámky)

Protože obě jádra procesoru RP2040 mohou přistupovat k určitému místu paměti současně, je potřeba, aby při zápisu věty byla daná pamět uzamčena a nemohlo ji číst jádro 0 ve stejný okamžik, kdy jádro 1 do ní zapisuje. Jádro 0 by mohlo přečíst nesmysly. Aby k tomu nedošlo, máme k dispozici tzv. mutexy (mutualy exclusive access), neboli zámky.

Mutexy mutex_t mtx; ve struktuře KRUHOVY_BUFFER souží k zamykání části paměti při zápisu nových dat do kruhového buferu funkcí seriova_linka() a také při zpracování věty a nastavení příznaku zpracovano ve funkci main().

K získání zámku (mutexu) se mohou použít funkce Pico C SDK mutex_enter_blocking(&mtx) anebo mutex_enter_timeout_ms(&mtx, ms). Pokud nelze získat mutex (paměť je zamčena jiným jádrem), funkce mutex_enter_blocking(&mtx) se zablokuje a bude čekat, dokud mutex nezíská. V případě získání zámku funkce vrací true.

Funkce mutex_enter_timeout_ms( &mtx, ms) se zablokuje jenom na daný počet milisekund a potom to eventuelně vzdá a vrátí false. Pří získání zámku funkce vrací true.

Funkce mutex_exit(&mtx) ukončuje výhradní přístup (vrací klíč k zámku).

Zápis nové NMEA věty v cyklu ve funkci seriova_linka()
if( zacatek && konec && nmea_checksum(buf,i) ) {
      // získání výhradního přístupu k části kruhového bufferu
      // pokud funkce nezíská zámek (mutex), dojde k dočasnému zablokování
      mutex_enter_blocking(&kbptr->mtx);
      // manipulace s daty
      strncpy(kbptr->data, buf, KBDATA_LEN);
      kbptr->valid = true;
      kbptr->zpracovano = false;
      kbptr->len = i;
      // uvolnění výhradního přístupu
      mutex_exit(&kbptr->mtx);
      // .. pokračování
}
Čtení NMEA věty v cyklu zpracování ve funkci main()
// čekání na mutex je omezeno 1 ms, pokud není získán mutex, tak se věta vypustí
// počká se a bude se zpracovávat další věta
if( mutex_enter_timeout_ms(&kbptr->mtx,1) ) {
    len = kbptr->len;
    strncpy(buf,&kbptr->data[0],KBDATA_LEN);
    kbptr->valid = false;
    kbptr->zpracovano = true;
    // uvolnění zámku
    mutex_exit(&kbptr->mtx);
    // ... pokračování
    }

Je dobrou programátorskou praktikou uvolnit použitý mutex funkcí mutex_exit(&mtx) co nejdříve. Pokud bychom v obou vláknech programu použili funkce mutex_enter_blocking( &mtx), tak se může stát, že obě vlákna budou čekat na získání mutexu nekonečně dlouho a program se zakousne dokud Pico nevypneme. To je pochopitelně něžádoucí stav.

Zpracování sebraných dat

Zpracování sebraných dat se provádí v cyklu for() funkce main(). Toto zpracování se každých 100 ms přeruší a výsledky se vypisují na dipleji. Z kruhového bufferu e vybírají pořád dokola nezpracované věty a některé z nich (GNZDA — čas, GNGGA — souřadnice a GNVTG — kurs a rychlost) se zpracovávají ve funkcích parse_gnzda(), parse_gngga() a parse_gnvtg(). Tyto funkce vytahují užitečné informace ke zobrazení na dipleji. Vybírání dat z kruhového bufferu je ošetřeno získáním mutexu.

Parsovací funkce pro zpracování informací z NMEA vět jsou v souborech parse_nmea.h (deklarace struktur a prototypy) a parse_nmea.c (implementace funkcí).

Výpis požadovaných dat je na grafický monochromatický displej s čipem ST7920 a má dvě obrazovky. První obrazovka se zeměpisnými souřadnicemi a časem a druhá obrazovka s rychlostí, kursem a časem. Přepínání obrazovek se provádí tlačítkem připojeným na GPIO pin 21.

    while( true ) {
        // zpracovani nezpracovanych dat (každých 100ms zpracování přerušíme
        // a budeme zobrazovat informace na displeji)
        for(uint64_t t = time_us_64(); (time_us_64() - t) < 100000; kbptr=kbptr->next) {
            chsum = false;
            if( kbptr->valid && kbptr->zpracovano == false  ) { // dosud nezpracované věty
                if( mutex_enter_timeout_ms(&kbptr->mtx,1) ) {
                    len = kbptr->len;
                    strncpy(buf,&kbptr->data[0],KBDATA_LEN);
                    kbptr->valid = false;
                    kbptr->zpracovano = true;
                    mutex_exit(&kbptr->mtx);
                    chsum = true;
                    // nastavení času
                    if( buf[0] == '$' && buf[1] == 'G' && buf[2] == 'N'
                        && buf[3] == 'Z' && buf[4] == 'D' && buf[5] == 'A' ) {
                        GNZDA gnzda = nmea_gnzda(buf, len);
                        // čas stačí nastavit jednou a potom každou hodinu
                        if( gnzda.parse_ok == 6 && (ctim.year == 0 | ctim.min == 0)) {
                            ctim.year  = gnzda.year;
                            ctim.month = gnzda.month;
                            ctim.day   = gnzda.day;
                            ctim.dotw  = gnzda.dotw;
                            ctim.hour  = gnzda.hour;
                            ctim.min   = gnzda.min;
                            ctim.sec   = gnzda.sec;
                            rtc_set_datetime(&ctim);
                            // vynulejeme chyby
                            chyby = 0;
                        }
                    }
                    // zeměpisné souřadnice
                    if( buf[0] == '$' && buf[1] == 'G' && buf[2] == 'N'
                        && buf[3] == 'G' && buf[4] == 'G' && buf[5] == 'A' ) {
                        gngga = nmea_gngga(buf,len);
                        // gngga.fix == 0 jsou neplatná data
                        if( gngga.parse_ok == 14 && gngga.fix > 0 ) {
                            memcpy( &st_gga, &gngga, sizeof(gngga));
                            break;
                        } else {
                            chyby++;
                        }
                    }
                    // kurs a rychlost
                    if( buf[0] == '$' && buf[1] == 'G' && buf[2] == 'N'
                        && buf[3] == 'V' && buf[4] == 'T' && buf[5] == 'G' ) {
                        gnvtg = nmea_gnvtg(buf,len);
                        if( gnvtg.parse_ok >= 7 ) {
                            memcpy( &stable_gnvtg, &gnvtg, sizeof(VTG));
                            break;
                        }
                    }
                }

            } else {
                sleep_ms(10);
                continue;
            }
        } // for

        // test tlačítka přepínání obrazovek
        if( gpio_get_events(TLAC_PIN) & GPIO_IRQ_EDGE_RISE ) {
            gpio_clear_events(TLAC_PIN, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL );
            disp_stav = disp_stav ? false : true;
        }
        // tisk na displeji
        if( disp_stav ) { // první obrazovka
            rtc_get_datetime(&ctim);
            datetime_to_str(datetime_str, sizeof(datetime_buf), &ctim);
            u8g2_ClearDisplay(&u8g2);
            u8g2_SetFont(&u8g2, u8g2_font_spleen8x16_me);
            sprintf(dbuf,"Šíř: %f˚ %c",st_gga.latitude, st_gga.latitude_ch);
            u8g2_DrawUTF8(&u8g2, 0, 16, dbuf);
            sprintf(dbuf,"Dél: %f˚ %c",st_gga.longitude, st_gga.longitude_ch);
            u8g2_DrawUTF8(&u8g2, 0, 32, dbuf);
            sprintf(dbuf,"Výš: %5.1f%c",st_gga.altitude, st_gga.altitude_unit=='M'?'m':' ');
            u8g2_uint_t width = u8g2_GetUTF8Width(&u8g2, dbuf);
            u8g2_DrawUTF8(&u8g2, 0, 48, dbuf);
            u8g2_SetFont(&u8g2, u8g2_font_spleen5x8_me);
            sprintf(dbuf,"%d sat.",st_gga.satelites);
            u8g2_DrawUTF8(&u8g2, width+5, 48, dbuf);
            sprintf(dbuf,"%2d.%2d.%4d %02d:%02d:%02d UTC %d",
                    ctim.day, ctim.month, ctim.year, ctim.hour, ctim.min, ctim.sec, chyby);
            u8g2_DrawUTF8(&u8g2, 0, 60, dbuf);
            u8g2_UpdateDisplay(&u8g2);
        } else { // druhá obrazovka
            rtc_get_datetime(&ctim);
            datetime_to_str(datetime_str, sizeof(datetime_buf), &ctim);
            u8g2_ClearDisplay(&u8g2);
            u8g2_SetFont(&u8g2, u8g2_font_spleen8x16_me);
            sprintf(dbuf,"Rych: %4.1f km/h", gnvtg.kmh);
            u8g2_DrawUTF8(&u8g2, 0, 16, dbuf);
            sprintf(dbuf,"Kurs: %f", gnvtg.kurs);
            u8g2_DrawUTF8(&u8g2, 0, 32, dbuf);
            sprintf(dbuf,"Výš: %5.1f%c",st_gga.altitude, st_gga.altitude_unit=='M'?'m':' ');
            u8g2_uint_t width = u8g2_GetUTF8Width(&u8g2, dbuf);
            u8g2_DrawUTF8(&u8g2, 0, 48, dbuf);
            u8g2_SetFont(&u8g2, u8g2_font_spleen5x8_me);
            sprintf(dbuf,"%d sat.",st_gga.satelites);
            u8g2_DrawUTF8(&u8g2, width+5, 48, dbuf);
            sprintf(dbuf,"%2d.%2d.%4d %02d:%02d:%02d UTC %d",
                    ctim.day, ctim.month, ctim.year, ctim.hour, ctim.min, ctim.sec, chyby);
            u8g2_DrawUTF8(&u8g2, 0, 60, dbuf);
            u8g2_UpdateDisplay(&u8g2);
        }
        sleep_ms(100);
        gpio_put(LED_PIN, led_stav);
        led_stav = led_stav ? false : true;
    }

Přinucení modulu L76K pracovat rychlostí 115200 baudů

Přednastavená rychlost posílání dat z modulu L76K je 9600 baudů. To je docela málo v některých situacích. V manuálu se dočteme, že NMEA větou $PCAS01,n*, kterou pošleme do L76K, můžeme přenastavit komunikační rychlost po sériové lince. Číslice n určuje rychlost:

  • 0 = 4800 baudů

  • 1 = 9600 baudů (přednastaveno)

  • 2 = 19200 baudů

  • 3 = 38400 baudů

  • 4 = 57600 baudů

  • 5 = 115200 baudů

Bohužel to nefunguje, jak si výrobce představoval.

Ještě existuje podobný příkaz PMTK251,r,* kde r je přímo komunikařní rychlost v baudech.

Postup by měl být takový, že máme nastavenou rychlost UART rozhraní v Picu na 9600 baudů, pošleme příkaz $PCAS01,5*19 a přenastavíme si linku na 115200 baudů. To nefunguje a modul na to nereaguje.

Po mnoha pokusech a pátrání po internetu je mi podařilo objevit postup, jak modul ke změně přinutit. Napadlo mě poslat tento příkaz několikrát po sobě a až potom změnit komunikační rychlost na Picu funkcí uart_set_baudrate(UART_ID, 115200). A ejhle, ono to funguje.

Zdlouhavé přinucení přepnout komunikační rychlost na 115200 baudů
    seriova_linka_init();
    uart_set_translate_crlf(UART_ID,true);
    memset(&buf[0],'\0',BUFLEN);

    // změna nastavení sériové linky na 115200 baudů
    // musí se to poslat vícekrát, protože na jedno nastavení modul
    // L76K nereaguje
    for(int i = 0; i<10 ; i++ ) {
        sprintf(buf,"$PCAS01,5*%02X\r\n\r\n", nmea_calc_checksum("$PCAS01,5*"));
        //printf("Posílám: %s", buf);
        uart_puts(UART_ID,buf);
        sleep_ms(10);
        sprintf(buf,"$PMTK251,115200*%02X\r\n\r\n", nmea_calc_checksum("$PMTK251,115200*"));
        //printf("Posílám: %s", buf);
        uart_puts(UART_ID,buf);
        sleep_ms(10);
    }

    int baudrate = uart_set_baudrate(UART_ID, 115200 );
    printf("Nastavuji UART na 115200\n");
    sleep_ms(1000);
    // ještě jednou sériová linka na 115200
    sprintf(buf,"$PCAS01,5*%02X\r\n\r\n", nmea_calc_checksum("$PCAS01,5*"));
    printf("Posílám: %s", buf);
    uart_puts(UART_ID,buf);
    sleep_ms(10);
    // aktualizace satelitů s frekvencí 5 Hz
    sprintf(buf,"$PCAS02,200*%02X\r\n\r\n", nmea_calc_checksum("$PCAS02,200*"));
    printf("Posílám: %s", buf);
    uart_puts(UART_ID,buf);
    sleep_ms(100);
    // sériová linka na 115200
    sprintf(buf,"$PMTK251,115200*%02X\r\n\r\n", nmea_calc_checksum("$PMTK251,115200*"));
    printf("Posílám: %s", buf);
    uart_puts(UART_ID,buf);
    sleep_ms(10);
    // smetí zahodíme
    for( int j=0; j<3; j++ ) {
        uart_read_blocking(UART_ID, buf, BUFLEN-2);
        buf[BUFLEN-1] = '\0';
        if(strstr(buf,"$PCAS01,5*19")) {
            printf("Sériová linka úspěšně přepnuta na %d baudů.\n", baudrate);
        }
        if(strstr(buf,"$PMTK251,115200*")) {
            printf("Sériová linka úspěšně přepnuta na %d baudů.\n", baudrate);
        }
        memset(&buf[0],'\0',BUFLEN);
    }

Není to sice programátorsky čisté, ale mohl jsem díky tomuto postupu přinutit modul L76K, aby informace ze setelitů občerstvoval s rychlostí 5Hz (5x za sekundu). To se dělá příkazem: $PCAS02,200*.

Věty, posílané do modulu, musejí být zabezpečeny kontrolním součtem. Na to jsem si vytvořil funkci:

// vypočte kontrolní součet NMEA věty
uint8_t nmea_calc_checksum(const char *data)
{
int len = strlen(data);
uint8_t checksum = (uint8_t) data[1]; // 0. znak věty je $, přeskočíme ho

    for(int i = 2; data[i]!='*' && i<len; i++) {
        checksum ^= (uint8_t) data[i];
    }
return checksum;
}

Zapojení projektu

Schéma zapojení v Kicadu 9.0

navigace schema zapojeni

Obrazovka 1 fungující navigace

IMG 20251028 175419

Obrazovka 2 fungující navigace

IMG 20251028 175602

GPS modul L76K nechtěl korektně chodit při napětí 5.0V, musím se napájet 5.1 V. Je to možná způsobeno dlouhým kabelem (4m) mezi Picem a modulem L76K. Na spájené verzi se chyba nevyskytuje. Takže to byl stoprocentně problém s kontakty.

Program

Sběr dat, zpracování NMEA vět a výpis na dipleji, funkce main().

navigace.c
/* navigace.c verze 0.11
 * (c) Jirka Chráska 2025, <jirka@lixis.cz>
 * Jednoduchá navigace
 * GPS data se získávají z modulu L76K sériovou linkou a ukládají do
 * kruhového bufferu pomocí vlákna na core1
 * core0 zpracovává data a zobrazuje na displeji
 * BSD 3-clause licence
 */
#include <stdio.h>
#include <stdlib.h>
#include "pico/stdlib.h"
#include "pico/multicore.h"
#include <string.h>
#include "hardware/uart.h"
#include "hardware/rtc.h"
#include "hardware/pwm.h"
#include "hardware/structs/iobank0.h"
#include "pico/util/datetime.h"
#include <u8g2.h>
#include "st7920_spi_u8g2_hal.h"
#include "podsvit.h"
#include "nmea_parse.h"


# define DEBUG 1

#define UBUF_LEN 9600+4
#define KBDATA_LEN 128
#define POCET_VET 75

uint8_t uart_buf[UBUF_LEN];

typedef struct kb {
    struct kb*          next;
    char*               data;
    int                 len;
    volatile bool       valid;
    volatile bool       zpracovano;
    mutex_t             mtx;
} KRUHOVY_BUFFER;

KRUHOVY_BUFFER kbdata[POCET_VET];


#define UART_ID         uart0
#define BAUD_RATE       9600
#define DATA_BITS       8
#define STOP_BITS       1
#define PARITY          UART_PARITY_NONE
#define UART_TX_PIN     0
#define UART_RX_PIN     1

// struktura pro kreslení na displej
u8g2_t u8g2;

 // inicializace UART
void seriova_linka_init(void)
{
    uart_init(UART_ID, BAUD_RATE);
    gpio_set_function( UART_TX_PIN, UART_FUNCSEL_NUM(UART_ID, UART_TX_PIN ));
    gpio_set_function( UART_RX_PIN, UART_FUNCSEL_NUM(UART_ID, UART_RX_PIN ));
    int actual = uart_set_baudrate(UART_ID, BAUD_RATE );
    uart_set_hw_flow( UART_ID, false, false );
    uart_set_fifo_enabled( UART_ID, true );
    memset(uart_buf,'\0',UBUF_LEN);    
}

// inicializace kruhoveho bufferu
void kruhovy_buffer_init(void)
{
    for( int j = 0; j<POCET_VET; j++)  { 
        kbdata[j].valid      = false; 
        kbdata[j].zpracovano = false;
        kbdata[j].data       = &uart_buf[j*KBDATA_LEN];
        kbdata[j].next       = &kbdata[j+1];
        kbdata[j].len        = 0;
        mutex_init(&kbdata[j].mtx);
    }
    kbdata[POCET_VET-1].next = &kbdata[0];
}
// sběr dat ze sériové linky (běží na core1)
void seriova_linka( void )
{
int i = 0;     // znak v NMEA větě 
uint8_t ch;    // znak ze sériové linky
KRUHOVY_BUFFER* kbptr = &kbdata[0];
char buf[KBDATA_LEN];
bool zacatek = false;
bool konec = false;
    
    kruhovy_buffer_init();
    
    while( true ) {
        memset(buf,'\0',KBDATA_LEN);
        while( ch = uart_getc(UART_ID) ) {
            if( i > KBDATA_LEN - 1 ) {  // vynucený konec věty
                i = 0;
                zacatek = false;
                konec   = false;
                buf[0]  = '\0';
                mutex_enter_blocking(&kbptr->mtx);
                kbptr->valid = false;
                mutex_exit(&kbptr->mtx);
                kbptr = kbptr->next;
            }
            if( ch == '$' ) {       // začátek věty
                buf[i]   = ch;
                buf[i+1] = '\0';
                i++;
                zacatek = true;
            } else if( ch == '\n' ) { // konec věty
                buf[i] = '\0';
                konec = true;
                if( zacatek && konec && nmea_checksum(buf,i) ) {
                    mutex_enter_blocking(&kbptr->mtx);
                    strncpy(kbptr->data, buf, KBDATA_LEN);
                    kbptr->valid = true;
                    kbptr->zpracovano = false;
                    kbptr->len = i;
                    mutex_exit(&kbptr->mtx);
                }
                kbptr = kbptr->next;
                i=0;
                zacatek = false;
                konec = false;
                break;
            } else if( ch == '\r' ) { // vynecháváme
                
            } else {
                 if( zacatek ) { 
                    buf[i]   = ch;
                    buf[i+1] = '\0';
                    i++;
                 }
            }
        } // uart_getc()
    } // while(true)
}
// -------------------------------------------------------------------------------------

#define BUFLEN 128
#define LED_PIN 25
#define TLAC_PIN 21
#define PODSVIT_PIN 22

uint32_t gpio_get_events(uint gpio)
{
    int32_t mask = 0xF << 4 * ( gpio % 8 );
return (iobank0_hw->intr[gpio / 8] & mask) >> 4 * ( gpio % 8 );
}

void gpio_clear_events(uint gpio, uint32_t events)
{
    gpio_acknowledge_irq(gpio, events);
}


int main(void)
{
bool led_stav = true;
bool disp_stav = true;
char buf[BUFLEN];
int len =  0;
int v   = 0;
char *ptr;
bool chsum = false;
GNGGA gngga;
GNGGA st_gga;
GLL gpgll;
VTG gnvtg = {0.0,0.0,0.0,'i',0};
VTG stable_gnvtg = {0.0,0.0,0.0,'i',0};
char dbuf[BUFLEN];
int sat = 1;
datetime_t ctim = {0,0,0,0,0,0,0}; // aktuální čas
char datetime_buf[256];
char *datetime_str = &datetime_buf[0];
KRUHOVY_BUFFER* kbptr = &kbdata[0];
int sekunda = 0;
double rychlost;
int chyby = 0;

#if DEBUG
    stdio_init_all();
#endif
    gpio_init( LED_PIN );
    gpio_set_function( LED_PIN, GPIO_FUNC_SIO );
    gpio_set_dir( LED_PIN, true );
    gpio_put(LED_PIN, led_stav);
    // tlacitko prepinani obrazovek
    gpio_set_function( TLAC_PIN, GPIO_FUNC_SIO );
    gpio_set_dir( TLAC_PIN, false );
    gpio_pull_down( TLAC_PIN );
    sleep_ms(1500);
#if DEBUG
    printf("\nGPS navigace v0.11 (c) Jirka Chráska 2025, <jirka@lixis.cz>\n\n");
#endif
    seriova_linka_init();
    uart_set_translate_crlf(UART_ID,true);
    memset(&buf[0],'\0',BUFLEN);

    // změna nastavení sériové linky na 115200 baudů
    // musí se to poslat vícekrát, protože na jedno nastavení modul
    // L76K nereaguje
    for(int i = 0; i<10 ; i++ ) {
        sprintf(buf,"$PCAS01,5*%02X\r\n\r\n", nmea_calc_checksum("$PCAS01,5*"));
        //printf("Posílám: %s", buf);
        uart_puts(UART_ID,buf);
        sleep_ms(10);
        sprintf(buf,"$PMTK251,115200*%02X\r\n\r\n", nmea_calc_checksum("$PMTK251,115200*"));
        //printf("Posílám: %s", buf);
        uart_puts(UART_ID,buf);
        sleep_ms(10);
    }
    
    int baudrate = uart_set_baudrate(UART_ID, 115200 );
    printf("Nastavuji UART na 115200\n");
    sleep_ms(1000);
    // ještě jednou sériová linka na 115200
    sprintf(buf,"$PCAS01,5*%02X\r\n\r\n", nmea_calc_checksum("$PCAS01,5*"));
    printf("Posílám: %s", buf);
    uart_puts(UART_ID,buf);
    sleep_ms(10);
    // aktualizace satelitů s frekvencí 5 Hz
    sprintf(buf,"$PCAS02,200*%02X\r\n\r\n", nmea_calc_checksum("$PCAS02,200*"));
    printf("Posílám: %s", buf);
    uart_puts(UART_ID,buf);
    sleep_ms(100);
    // sériová linka na 115200
    sprintf(buf,"$PMTK251,115200*%02X\r\n\r\n", nmea_calc_checksum("$PMTK251,115200*"));
    printf("Posílám: %s", buf);
    uart_puts(UART_ID,buf);
    sleep_ms(10);
    // smetí zahodíme
    for( int j=0; j<3; j++ ) {
        uart_read_blocking(UART_ID, buf, BUFLEN-2);
        buf[BUFLEN-1] = '\0';
        if(strstr(buf,"$PCAS01,5*19")) {
            printf("Sériová linka úspěšně přepnuta na %d baudů.\n", baudrate);
        }
        if(strstr(buf,"$PMTK251,115200*")) {
            printf("Sériová linka úspěšně přepnuta na %d baudů.\n", baudrate);
        }
        memset(&buf[0],'\0',BUFLEN);
    }

    memset(&st_gga,0,sizeof(GNGGA));
    // nastavení displeje
    u8g2_Setup_st7920_s_128x64_f(&u8g2, U8G2_R0, 
                                 u8x8_byte_pico_hw_spi, u8x8_gpio_and_delay_pico);
    u8g2_InitDisplay(&u8g2);
    // nastavení podsvitu displeje
    podsvit_init(PODSVIT_PIN);
    podsvit(60);
    // výmaz displeje
    u8g2_ClearDisplay(&u8g2);
    u8g2_SetDrawColor(&u8g2, 1);
    u8g2_SetFont(&u8g2, u8g2_font_simple1_te);
    sprintf(dbuf,"Jednoduchá GPS navigace");
    u8g2_DrawUTF8(&u8g2, 0, 14, dbuf);
    sprintf(dbuf,"(c) Jirka Chráska 2025");
    u8g2_DrawUTF8(&u8g2, 0, 28, dbuf);
    sprintf(dbuf,"<jirka@lixis.cz>");
    u8g2_DrawUTF8(&u8g2, 0, 42, dbuf);
    sprintf(dbuf,"UART %d. Hledám satelity...",baudrate);
    u8g2_DrawUTF8(&u8g2, 0, 58, dbuf);
    u8g2_UpdateDisplay(&u8g2);  
    
    // inicializace hodin reálného času
    rtc_init();
    sleep_ms(5000);
    // nahodíme sbírání dat na core1
    multicore_launch_core1(seriova_linka);
    sleep_ms(100);
    
    while( true ) {
        // zpracovani nezpracovanych dat (každých 100ms zpracování přerušíme 
        // a budeme zobrazovat informace na displeji)
        for(uint64_t t = time_us_64(); (time_us_64() - t) < 100000; kbptr=kbptr->next) { 
            chsum = false;
            if( kbptr->valid && kbptr->zpracovano == false  ) {
                if( mutex_enter_timeout_ms(&kbptr->mtx,1) ) {
                    len = kbptr->len;
                    strncpy(buf,&kbptr->data[0],KBDATA_LEN);
                    kbptr->valid = false;
                    kbptr->zpracovano = true;
                    mutex_exit(&kbptr->mtx);
                    chsum = true;
#if DEBUG
                    printf("Věta %p; len=%3d %18s '%s'\n", 
                    kbptr, len, chsum ? "Correct data:" : "Checksum error:", buf);
#endif
                    // nastavení času
                    if( buf[0] == '$' && buf[1] == 'G' && buf[2] == 'N' 
                        && buf[3] == 'Z' && buf[4] == 'D' && buf[5] == 'A' ) {
                        GNZDA gnzda = nmea_gnzda(buf, len);
                        // čas stačí nastavit jednou a potom každou hodinu
                        if( gnzda.parse_ok == 6 && (ctim.year == 0 | ctim.min == 0)) {
                            ctim.year  = gnzda.year;
                            ctim.month = gnzda.month;
                            ctim.day   = gnzda.day;
                            ctim.dotw  = gnzda.dotw;
                            ctim.hour  = gnzda.hour;
                            ctim.min   = gnzda.min;
                            ctim.sec   = gnzda.sec;
                            rtc_set_datetime(&ctim);
                            // vynulejeme chyby
                            chyby = 0;
                        }
                    } 
                    // zeměpisné souřadnice
                    if( buf[0] == '$' && buf[1] == 'G' && buf[2] == 'N'  
                        && buf[3] == 'G' && buf[4] == 'G' && buf[5] == 'A' ) {
                        gngga = nmea_gngga(buf,len);
                        // gngga.fix == 0 jsou neplatná data
                        if( gngga.parse_ok == 14 && gngga.fix > 0 ) { 
                            memcpy( &st_gga, &gngga, sizeof(gngga));
                            break;
                        } else {
                            chyby++;
                        }
                    } 
                    // kurs a rychlost 
                    if( buf[0] == '$' && buf[1] == 'G' && buf[2] == 'N'  
                        && buf[3] == 'V' && buf[4] == 'T' && buf[5] == 'G' ) {
                        gnvtg = nmea_gnvtg(buf,len);
                        if( gnvtg.parse_ok >= 7 ) {
                            memcpy( &stable_gnvtg, &gnvtg, sizeof(VTG));
                            break;
                        }
                    } 
                }
                
            } else {
                sleep_ms(10);
                continue;
            }
        } // for

        // test tlačítka přepínání obrazovek
        if( gpio_get_events(TLAC_PIN) & GPIO_IRQ_EDGE_RISE ) {
            gpio_clear_events(TLAC_PIN, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL );
            disp_stav = disp_stav ? false : true; 
        }
        // tisk na displeji
        if( disp_stav ) { // první obrazovka
            rtc_get_datetime(&ctim);
            datetime_to_str(datetime_str, sizeof(datetime_buf), &ctim);
            u8g2_ClearDisplay(&u8g2);
            u8g2_SetFont(&u8g2, u8g2_font_spleen8x16_me);
            sprintf(dbuf,"Šíř: %f˚ %c",st_gga.latitude, st_gga.latitude_ch);
            u8g2_DrawUTF8(&u8g2, 0, 16, dbuf);
            sprintf(dbuf,"Dél: %f˚ %c",st_gga.longitude, st_gga.longitude_ch);
            u8g2_DrawUTF8(&u8g2, 0, 32, dbuf);
            sprintf(dbuf,"Výš: %5.1f%c",st_gga.altitude, st_gga.altitude_unit=='M'?'m':' ');
            u8g2_uint_t width = u8g2_GetUTF8Width(&u8g2, dbuf);
            u8g2_DrawUTF8(&u8g2, 0, 48, dbuf);
            u8g2_SetFont(&u8g2, u8g2_font_spleen5x8_me);
            sprintf(dbuf,"%d sat.",st_gga.satelites);
            u8g2_DrawUTF8(&u8g2, width+5, 48, dbuf);
            sprintf(dbuf,"%2d.%2d.%4d %02d:%02d:%02d UTC %d", 
                    ctim.day, ctim.month, ctim.year, ctim.hour, ctim.min, ctim.sec, chyby);
            u8g2_DrawUTF8(&u8g2, 0, 60, dbuf);
            u8g2_UpdateDisplay(&u8g2);
        } else { // druhá obrazovka
            rtc_get_datetime(&ctim);
            datetime_to_str(datetime_str, sizeof(datetime_buf), &ctim);
            u8g2_ClearDisplay(&u8g2);
            u8g2_SetFont(&u8g2, u8g2_font_spleen8x16_me);
            sprintf(dbuf,"Rych: %4.1f km/h", gnvtg.kmh);
            u8g2_DrawUTF8(&u8g2, 0, 16, dbuf);
            sprintf(dbuf,"Kurs: %f", gnvtg.kurs);
            u8g2_DrawUTF8(&u8g2, 0, 32, dbuf);
            sprintf(dbuf,"Výš: %5.1f%c",st_gga.altitude, st_gga.altitude_unit=='M'?'m':' ');
            u8g2_uint_t width = u8g2_GetUTF8Width(&u8g2, dbuf);
            u8g2_DrawUTF8(&u8g2, 0, 48, dbuf);
            u8g2_SetFont(&u8g2, u8g2_font_spleen5x8_me);
            sprintf(dbuf,"%d sat.",st_gga.satelites);
            u8g2_DrawUTF8(&u8g2, width+5, 48, dbuf);
            sprintf(dbuf,"%2d.%2d.%4d %02d:%02d:%02d UTC %d", 
                    ctim.day, ctim.month, ctim.year, ctim.hour, ctim.min, ctim.sec, chyby);
            u8g2_DrawUTF8(&u8g2, 0, 60, dbuf);
            u8g2_UpdateDisplay(&u8g2);
        }
        sleep_ms(100);
        gpio_put(LED_PIN, led_stav);
        led_stav = led_stav ? false : true;
    }
}

Parsování posílaných dat z modulu L76K po sériové lince, protokol NMEA 0183.

nmea_parse.h
/* nmea_parse.h
 * (c) Jirka Chráska 2025; <jirka@lixis.cz>
 * BSD 3-clause licence
 */

// parsování věty $GNZDA,155647.000,19,10,2025,00
// 15 jsou hodiny
// 56 jsou minuty
// 47.000 jsou sekundy
// 19 je den v měsíci
// 10 je měsíc
// 2025 je rok
// 00 je den v týdnu, začíná se nedělí
// informace o světovém času (UTC) podle Gregoriánského kalendáře
typedef struct {
        int year;
        int month;
        int day;
        int dotw;   // pořadí dne v týdnu 0 je neděle
        int hour;
        int min;
        int sec;
        int msec;
        int parse_ok;
} GNZDA;
/* Indexy */
#define NMEA_GNZDA_TIME                 1
#define NMEA_GNZDA_DAY                  2
#define NMEA_GNZDA_MONTH                3
#define NMEA_GNZDA_YEAR                 4
#define NMEA_GNZDA_DAYOFWEEK            5


GNZDA nmea_gnzda( char *data, int len );

//---------------------------------------------------
// informace o zeměpisných souřadnicích
typedef struct {
    double  time;
    int     time_hour;
    int     time_min;
    int     time_sec;
    int     time_msec;
    double  latitude;
    uint8_t latitude_ch;
    int     latitude_degree;
    int     latitude_minutes;
    double  latitude_vteriny;
    double  longitude;
    uint8_t longitude_ch;
    int     longitude_degree;
    int     longitude_minutes;
    double  longitude_vteriny;
    int     fix;
    int     satelites;
    double  horizontal_dilution;
    double  altitude;
    uint8_t altitude_unit;
    double  geoid_height;
    uint8_t geoid_height_unit;
    uint8_t checksum;
    bool    checksum_ok;
    int     parse_ok;
} GNGGA;

/* Indexy */
#define NMEA_GPGGA_TIME                 1
#define NMEA_GPGGA_LATITUDE             2
#define NMEA_GPGGA_LATITUDE_CARDINAL    3
#define NMEA_GPGGA_LONGITUDE            4
#define NMEA_GPGGA_LONGITUDE_CARDINAL   5
#define NMEA_GPGGA_POSITION_FIX         6
#define NMEA_GPGGA_N_SATELLITES         7
#define NMEA_GPGGA_HORIZ_DILUTION       8
#define NMEA_GPGGA_ALTITUDE             9
#define NMEA_GPGGA_ALTITUDE_UNIT        10
#define NMEA_GPGGA_UNDULATION           11
#define NMEA_GPGGA_UNDULATION_UNIT      12

// parsování věty $GNGGA
GNGGA nmea_gngga( char *data, int len );
// -------------------------------------------------
typedef struct {
        double  latitude;
        char    latitude_ch;
        double  longitude;
        char    longitude_ch;
        char    valid;
        double  time;
        int     time_hour;
        int     time_min;
        int     time_sec;
        int     time_msec;
        int     parse_ok;
} GLL;
/* Indexy */
#define NMEA_GPGLL_LATITUDE             1
#define NMEA_GPGLL_LATITUDE_CARDINAL    2
#define NMEA_GPGLL_LONGITUDE            3
#define NMEA_GPGLL_LONGITUDE_CARDINAL   4
#define NMEA_GPGLL_TIME                 5
#define NMEA_GPGLL_VALID_DATA           6

GLL nmea_gpgll( char *data, int len );
// -------------------------------------------------
// GPVTG -- kurs a rychlost
typedef struct {
        double  kurs;
        double  uzly;
        double  kmh;
        char    mode;
        int     parse_ok;
} VTG; 

#define NMEA_GPVTG_COURSE_AZIMUT                1
#define NMEA_GPVTG_COURSE_AZIMUT_CARDINAL       2
#define NMEA_GPVTG_COURSE_MAGNETIC              1000
#define NMEA_GPVTG_COURSE_MAGNETIC_CARDINAL     3
#define NMEA_GPVTG_SPEED_KNOTS                  4
#define NMEA_GPVTG_SPEED_KNOTS_CARDINAL         5
#define NMEA_GPVTG_SPEED_KMPERHOUR              6
#define NMEA_GPVTG_SPEED_KMPERHOUR_CARDINAL     7
#define NMEA_GPVTG_MODE_INDICATOR               8

VTG nmea_gnvtg( char *data, int len );
// -------------------------------------------------
bool nmea_checksum(const char *data, int len );
// vypočte kontrolní součet NMEA věty
uint8_t nmea_calc_checksum(const char* data);
nmea_parse.c
/* nmea_parse.c 
 * (c) Jirka Chráska 2025, <jirka@lixis.cz>
 * BSD 3 clause licence
 */

#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "nmea_parse.h"


GNZDA nmea_gnzda( char *data, int len )
{
GNZDA res = {0, 0, 0, 0, 0, 0, 0};    
int section = 0;
char *str1;
char *saveptr1;
char *token;
char buf[10];

    for(section=0, str1 = data ; section<=5; section++, str1=NULL) {
        token = strtok_r(str1,",",&saveptr1);
        if( token == NULL ) {
            break;
            }
        switch ( section ) {
            case 0: // nazev
                if( strcmp(token,"$GNZDA") == 0 ) { res.parse_ok=1; }
                break;
            case NMEA_GNZDA_TIME: // cas
                buf[0] = token[0]; buf[1]=token[1]; buf[2] = '\0';
                res.hour = atoi(buf);
                buf[0] = token[2]; buf[1]=token[3]; buf[2] = '\0';
                res.min = atoi(buf);
                buf[0] = token[4]; buf[1]=token[5]; buf[2] = '\0';
                res.sec = atoi(buf);
                res.msec = atoi(&token[7]);
                res.parse_ok++;
                break;
            case NMEA_GNZDA_DAY: // den v měsíci
                res.day = atoi(token);
                res.parse_ok++;
                break;
            case NMEA_GNZDA_MONTH: // měsíc
                res.month = atoi(token);
                res.parse_ok++;
                break;
            case NMEA_GNZDA_YEAR: // rok
                res.year = atoi(token);
                res.parse_ok++;
                break;
            case NMEA_GNZDA_DAYOFWEEK: // den v týdnu
                res.dotw = atoi(token);
                res.parse_ok++;
                break;
            default:
                res.parse_ok = -1;
                break;
        }
    }
return res;    
}
//---------------------------------------------------
// parsování věty $GNGGA
GNGGA nmea_gngga( char *data, int len )
{
GNGGA res = {0.0, 0, 0, 0, 0, 0.0, ' ', 0.0, ' ', 0, 0, 0.0, 0.0, ' ', 0.0, ' ', '\0', false, 0};    
int section = 0;
char *str1;
char *saveptr1;
char *token;
char *buf[10];

    for(section=0, str1 = data ; section<=13; section++, str1=NULL) {
        token = strtok_r(str1,",*",&saveptr1);
        if( token == NULL ) {
            break;
            }
        switch ( section ) {
            case 0: // nazev
                if( strcmp(token,"$GNGGA") == 0 ) { res.parse_ok=1; }
                break;
            case NMEA_GPGGA_TIME: // cas
                res.time  = strtod(token, NULL);
                res.time_hour = ((int)res.time)/10000;
                res.time_min  = ((int)res.time - (res.time_hour*10000))/100;
                res.time_sec  = ((int)res.time - (res.time_hour*10000) - (res.time_min*100));
                res.time_msec = (res.time - res.time_hour*10000.0 - res.time_min*100.0 - res.time_sec)*1000;  
                res.parse_ok++;
                break;
            case NMEA_GPGGA_LATITUDE: // zeměpisná šířka ve stupních
                res.latitude = strtod(token,NULL)/100.0;
                res.latitude_degree  = atoi(token)/100;
                res.latitude_minutes = atoi(token+2); 
                res.latitude_vteriny = res.latitude - res.latitude_degree - res.latitude_minutes;
                if( res.latitude_vteriny < 0.0) { res.latitude_minutes += 1; res.latitude_vteriny += 60.0; }
                res.parse_ok++;
                break;
            case NMEA_GPGGA_LATITUDE_CARDINAL: // severní N nebo jižní S
                res.latitude_ch = token[0];
                res.parse_ok++;
                break;
            case NMEA_GPGGA_LONGITUDE: // zeměpisná délka
                res.longitude = strtod(token,NULL)/100.0;
                res.longitude_degree  = atoi(token)/100;
                res.longitude_minutes = (atoi(token) - res.longitude_degree*100)*6/10;
                res.longitude_vteriny = (res.longitude - res.longitude_degree*1.0 - res.longitude_minutes*6.0/10.0)*6.0/10.0;
                if( res.longitude_vteriny < 0.0) { res.longitude_minutes += 1; res.longitude_vteriny += 60.0; }
                res.parse_ok++;
                break;
            case NMEA_GPGGA_LONGITUDE_CARDINAL: // západní W nebo východní E
                res.longitude_ch = token[0];
                res.parse_ok++;
                break;
            case NMEA_GPGGA_POSITION_FIX: // fixace
                res.fix = atoi(token);
                res.parse_ok++;
                break;
            case NMEA_GPGGA_N_SATELLITES: // počet satelitů
                res.satelites = atoi(token);
                res.parse_ok++;
                break;
            case NMEA_GPGGA_HORIZ_DILUTION: // plavání chyby
                res.horizontal_dilution = strtod(token,NULL);
            case NMEA_GPGGA_ALTITUDE: // výška nad mořem
                res.altitude = strtod(token,NULL);
                res.parse_ok++;
                break;
            case NMEA_GPGGA_ALTITUDE_UNIT: // jednotka nadmořské výšky
                res.altitude_unit = token[0];
                res.parse_ok++;
                break;
            case NMEA_GPGGA_UNDULATION: // výška nad ideálním WGS84 elipsoidem
                res.geoid_height = strtod(token,NULL);
                res.parse_ok++;
                break;
            case NMEA_GPGGA_UNDULATION_UNIT: // jednotka výšky nad WGS84 elipsoidem
                res.geoid_height_unit = token[0];
                res.parse_ok++;
                break;
            case 13: // checksum
                res.checksum = atoi(token);
                res.parse_ok++;
                break;
            default:
                res.parse_ok = -1;
                break;
        }
    }
return res;
}
//---------------------------------------------------
GLL nmea_gpgll( char *data, int len )
{
GLL res = {0.0, ' ', 0.0, ' ', 'N', 0.0, 0, 0, 0 };
int section = 0;
char *str1;
char *saveptr1;
char *token;
//char buf[10];

    for(section=0, str1 = data ; section<=6; section++, str1=NULL) {
        token = strtok_r(str1,",*",&saveptr1);
        if( token == NULL ) {
            break;
            }
        switch ( section ) {
            case 0: // nazev
                if( strcmp(token,"$GPGLL") == 0 ) { res.parse_ok=1; }
                break;
            case NMEA_GPGLL_LATITUDE: // zeměpisná délka
                res.latitude = strtod(token,NULL)/100.0;
                res.parse_ok++;
                break;
            case NMEA_GPGLL_LATITUDE_CARDINAL: // N - severní, S - jižní
                res.latitude_ch = token[0];
                res.parse_ok++;
                break;
            case NMEA_GPGLL_LONGITUDE: // zeměpisná délka
                res.longitude = strtod(token,NULL)/100.0;
                res.parse_ok++;
                break;
            case NMEA_GPGLL_LONGITUDE_CARDINAL: // N - severní, S - jižní
                res.longitude_ch = token[0];
                res.parse_ok++;
                break;
            case NMEA_GPGLL_TIME: // toto je volitelné
                res.time = strtod(token,NULL);
                res.time_hour = ((int)res.time)/10000;
                res.time_min  = ((int)res.time - (res.time_hour*10000))/100;
                res.time_sec  = ((int)res.time - (res.time_hour*10000) - (res.time_min*100));
                res.time_msec = (res.time - res.time_hour*10000.0 - res.time_min*100.0 - res.time_sec)*1000;  
                res.parse_ok++;
                break;
            case NMEA_GPGLL_VALID_DATA: // A - data jsou platná
                res.valid = token[0];
                res.parse_ok++;
                break;
        }
                
    }
return res;
}
// ---------------------------------------------------
// kurs a rychlost GNVTG
VTG nmea_gnvtg( char *data, int len)
{
VTG res = { 0.0, 0.0, 0.0, '\0', 0};
int section = 0;
char *str1;
char *saveptr1;
char *token;

    for(section=0, str1 = data ; section<=9; section++, str1=NULL) {
        token = strtok_r(str1,",*",&saveptr1);
        if( token == NULL ) {
            break;
            }
        // printf("Sekce=%d token='%s'",section,token);
        switch ( section ) {
            case 0: // nazev
                if( strcmp(token,"$GNVTG") == 0 ) { res.parse_ok=1; }
                break;
            case NMEA_GPVTG_COURSE_AZIMUT: // azimut 
                res.kurs = strtod(token,NULL);
                res.parse_ok++;
                break;
            case NMEA_GPVTG_COURSE_AZIMUT_CARDINAL:  // písmenko T
                if( strcmp(token, "T") == 0 ) { res.parse_ok++; }
                break;
            case NMEA_GPVTG_COURSE_MAGNETIC: // tady nebude nic
                res.parse_ok++;
                break;
            case NMEA_GPVTG_COURSE_MAGNETIC_CARDINAL: // tady bude písmenko M
                if( strcmp(token, "M") == 0 ) { res.parse_ok++; }
                break;
            case NMEA_GPVTG_SPEED_KNOTS:  // rychlost v uzlech
                res.uzly = strtod(token,NULL);
                res.parse_ok++;
                break;
            case NMEA_GPVTG_SPEED_KNOTS_CARDINAL: // písmenko N
                if( strcmp(token, "N") == 0 ) { res.parse_ok++; }
                break;
            case NMEA_GPVTG_SPEED_KMPERHOUR: // rychlost v km/h
                res.kmh = strtod(token,NULL);
                res.parse_ok++;
                break;
            case NMEA_GPVTG_SPEED_KMPERHOUR_CARDINAL: // písmenko K
                if( strcmp(token, "K") == 0 ) { res.parse_ok++; }
                break;
            case  NMEA_GPVTG_MODE_INDICATOR: // A - autonomní režim, E odhad, N - neplatná data
                res.mode = token[0];
                res.parse_ok++;
                break;
        }
    }
return res;
}

// ---------------------------------------------------
// porovná kontrolní součet NMEA věty
bool nmea_checksum(const char *data, int len )
{
uint8_t checksum = 0;
uint8_t checksum_calculated = 1;
     
    if(len < 7 ) {
        return false; // příliš krátká věta
    }
    if(data[len-3] != '*') return false;
    if( ((data[len-2]>='0' && data[len-2]<='9') || (data[len-2]>='A' && data[len-2]<='F')) 
      && ((data[len-1]>='0' && data[len-1]<='9') || (data[len-1]>='A' && data[len-1]<='F'))) {
         checksum = strtol(&data[len-2],NULL,16);
         checksum_calculated = (uint8_t) data[1];
         for( int i=2; i<len-3; i++) {
             checksum_calculated ^= (uint8_t) data[i];
         }
    }
return checksum == checksum_calculated ? true : false;
}
// ---------------------------------------------------
// vypočte kontrolní součet NMEA věty
uint8_t nmea_calc_checksum(const char *data)
{
int len = strlen(data);
uint8_t checksum = (uint8_t) data[1]; // 0. znak věty je $, přeskočíme ho

    for(int i = 2; data[i]!='*' && i<len; i++) {
        checksum ^= (uint8_t) data[i];
    }
return checksum;
}
// ---------------------------------------------------

Ovladač hardware displeje.

st7920_spi_u8g2_hal.c
#include "st7920_spi_u8g2_hal.h"

void st7920_writeReg_SPI(uint8_t aByte)
{
    uint8_t cmdBuf[3];
    cmdBuf[0] = 0b11111000;
    cmdBuf[1] = aByte & 0xf0;
    cmdBuf[2] = (aByte & 0x0f) << 0x04;
    spi_write_blocking(SPI_PORT, cmdBuf, sizeof(cmdBuf));
}

void st7920_writeData_SPI(uint8_t aByte)
{
    uint8_t cmdBuf[3];
    cmdBuf[0] = 0b11111010;
    cmdBuf[1] = aByte & 0xf0;
    cmdBuf[2] = (aByte & 0x0f) << 0x04;
    spi_write_blocking(SPI_PORT, cmdBuf, sizeof(cmdBuf));
}

uint8_t u8x8_gpio_and_delay_pico(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
    switch (msg)
    {
    case U8X8_MSG_GPIO_AND_DELAY_INIT:
        gpio_init(PIN_RST);
        gpio_init(PIN_CS);
        
        gpio_set_dir(PIN_RST, GPIO_OUT);
        gpio_set_dir(PIN_CS, GPIO_OUT);

        gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
        gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);
        spi_init(SPI_PORT, SPI_SPEED);

        gpio_put(PIN_RST, 0);
        sleep_ms(100);
        gpio_put(PIN_RST, 1);

        gpio_put(PIN_CS, 1);

        break;
    case U8X8_MSG_DELAY_NANO: // delay arg_int * 1 nano second
        sleep_us(arg_int);    // 1000 times slower, though generally fine in practice given rp2040 has no `sleep_ns()`
        break;
    case U8X8_MSG_DELAY_100NANO: // delay arg_int * 100 nano seconds
        sleep_us(arg_int);
        break;
    case U8X8_MSG_DELAY_10MICRO: // delay arg_int * 10 micro seconds
        sleep_us(arg_int * 10);
        break;
    case U8X8_MSG_DELAY_MILLI: // delay arg_int * 1 milli second
        sleep_ms(arg_int);
        break;
    case U8X8_MSG_GPIO_CS: // CS (chip select) pin: Output level in arg_int
        gpio_put(PIN_CS, arg_int);
        break;
    case U8X8_MSG_GPIO_DC: // DC (data/cmd, A0, register select) pin: Output level
        break;
    case U8X8_MSG_GPIO_RESET:       // Reset pin: Output level in arg_int
        gpio_put(PIN_RST, arg_int); // printf("U8X8_MSG_GPIO_RESET %d\n", arg_int);
        break;
    default:
        u8x8_SetGPIOResult(u8x8, 1); // default return value
        break;
    }
    return 1;
}

uint8_t u8x8_byte_pico_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
    uint8_t *data;
    switch (msg)
    {
    case U8X8_MSG_BYTE_SEND:
        spi_write_blocking(SPI_PORT, (uint8_t*)arg_ptr, arg_int);
        break;
    case U8X8_MSG_BYTE_INIT:
        break;
    case U8X8_MSG_BYTE_SET_DC:
        break;
    case U8X8_MSG_BYTE_START_TRANSFER:
        gpio_put(PIN_CS, 1);
        break;
    case U8X8_MSG_BYTE_END_TRANSFER:
        gpio_put(PIN_CS, 0);
        break;
    default:
        return 0;
    }
    return 1;
}
st7920_spi_u8g2_hal.h
#ifndef ST7920_SPI_U8G2_HAL_H
#define ST7920_SPI_U8G2_HAL_H

// Reference: https://github.com/olikraus/u8g2/issues/2159

#include <u8g2.h>
#include "pico/stdlib.h"
#include "hardware/spi.h"

// zapojení displeje a parametry DPI sběrnice
#define SPI_PORT    spi0
#define PIN_CS      5
#define PIN_SCK     2
#define PIN_MOSI    3
#define SPI_SPEED   1000 * 1000
#define PIN_RST     13

void st7920_writeReg_SPI(uint8_t aByte);
void st7920_writeData_SPI(uint8_t aByte);
uint8_t u8x8_gpio_and_delay_pico(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
uint8_t u8x8_byte_pico_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);

#endif

Řízení podsvitu

podsvit.c
/* podsvit.c - řízení podsvitu displeje
 * (c) Jirka Chráska 2025; jirka@lixis.cz
 */
 
 
#include "pico/stdlib.h"
#include "hardware/pwm.h"

// PWM řízení podsvitu
static uint slice_num;
static uint chan;


// výpočet střídy
uint32_t pwm_set_freq_duty( uint slice_num, uint chan, uint32_t f, int d)
{
    uint32_t clock     = 125000000;
    uint32_t divider16 = clock / f / 4096 + (clock % (f * 4096) != 0);
    if( divider16 / 16 == 0) {
        divider16 = 16;
    }
    uint32_t wrap = clock * 16 / divider16 / f - 1;
    pwm_set_clkdiv_int_frac( slice_num, divider16/16, divider16 & 0xf );
    pwm_set_wrap( slice_num, wrap );
    pwm_set_chan_level( slice_num, chan, wrap * d/100 );
    return wrap;
}

// výpočet hodnoty wrap
uint32_t pwm_get_wrap( uint slice_num )
{
    valid_params_if(HARDWARE_PWM, slice_num >= 0 && slice_num < NUM_PWM_SLICES );
    return pwm_hw->slice[slice_num].top;
}

// nastavení střídy
void pwm_set_duty( uint slice_num, uint chan, int d)
{
    pwm_set_chan_level(slice_num, chan, pwm_get_wrap( slice_num) * d/100 );
}

void podsvit_init(int podsvit_pin)
{
    gpio_set_function( podsvit_pin, GPIO_FUNC_PWM );
    slice_num   = pwm_gpio_to_slice_num ( podsvit_pin );
    chan        = pwm_gpio_to_channel( podsvit_pin );

    uint wrap = pwm_set_freq_duty(slice_num, chan, 500, 0); // 200 Hz
    pwm_set_enabled (slice_num, true);
}

void podsvit(int procento)
{
int d = (procento*procento*procento)/10000;
    pwm_set_duty(slice_num, chan, d);
}
podsvit.h
/* podsvit.h
 *
 */
 
void podsvit_init(int podsvit_pin);
void podsvit(int procento);
CMakeLists.txt
cmake_minimum_required(VERSION 3.13)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffunction-sections -fdata-sections -Wl,--gc-sections")
# místo pico můžete napsat picow (Pico s Wifi) nebo pico2 (Pico 2 s procesorem RP3520)
set(PICO_BOARD pico CACHE STRING "Board type")

include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)

# nastaveni jmena projektu
project(navigace C CXX ASM)
# nastaveni jmena spustitelneho souboru (abychom to nemuseli stále opisovat)
set(EXE navigace)

#inicializace Raspberry Pi Pico SDK
pico_sdk_init()

add_executable(${EXE}
                st7920_spi_u8g2_hal.c
                navigace.c
                podsvit.c
                nmea_parse.c
                )
pico_set_program_name(${EXE} "navigace")
pico_set_program_version(${EXE} "0.10")

# nastavení standardního výstupu  0 znamená nepoužito, 1 znamená použito
pico_enable_stdio_uart(${EXE} 0)
pico_enable_stdio_usb(${EXE} 1)

# přidání standardní knihovny do projektu
target_link_libraries(${EXE}
                        pico_stdlib
                        )
# include adresáře
target_include_directories( ${EXE} PRIVATE
                            ${CMAKE_CURRENT_LIST_DIR}
                            ${CMAKE_CURRENT_LIST_DIR}/.. # pokud potřebujeme třeba pro lwipopts
                            u8g2/csrc
                            )
file(GLOB U8G2_SRC u8g2/csrc/*.c)
add_library(u8g2 ${U8G2_SRC})


# volby pro linker
target_link_options( ${EXE} PRIVATE -Xlinker --print-memory-usage)

# další knihovny, které budeme v projektu potřebovat
target_link_libraries( ${EXE}
                        hardware_spi    # hardware_spi potřebujeme pro displej
                        hardware_pwm    # hardware_pwm je pro řízení jasu displeje
                        u8g2            # u8g2 potřebujeme pro displej
                        # dále doplňte knihovny, které potřebujete
                        hardware_rtc    # hodiny reálného času
                        hardware_pwm    # řízení podsvitu
                        pico_multicore  # zapojení druhého jádra procesoru
                        )
# toto je potřeba pro generování uf2 souboru, který nahráváme do Pica
pico_add_extra_outputs(${EXE})
Sestavení projektu
mkdir build
cd build
cmake ..
make -j8
make

Celý projekt ke stažení navigace11.tar.gz 312900868 byte.

Zdroje a odkazy