Rasperry Pi Pico nemá hodiny reálného času zálohované baterií, proto je nutné vždy po startu nastavovat čas. A kde vzít přesný čas? Máme dvě možnosti: připojit GPS modul, který poskytuje přesný čas ze satelitů GPS anebo dotázat se na přesný čas SNTP serveru. Druhá možnost je popsána v článku. Vše postavíme na IPv6 protokolu.

SNTP je zkratka Simple Network Time Prococol a tímto protokolem se přesně nastavují hodiny reálného času na zařízení.

Nechtělo se mi dávat ESSID, heslo k wifi a jméno hosta přímo do kódu, tak jsem to vyřešil pomocí CMake. Dělá se to tak, že WIFI_SSID, WIFI_PASSWORD a PICO_HOSTNAME se nadefinují jako proměnné shellu (anebo přímo do CMakeLists.txt, což není pěkné)

Potom se sestavení projektu dělá takto:

export WIFI_SSID="eesid_kam_se_připojujeme"
export WIFI_PASSWORD="skutečnéheslo"
export PICO_HOSTNAME="picow-hodiny"
mkdir build
cd build
cmake ..
make -j8

A hodnoty proměnných WIFI_SSID, WIFI_PASSWORD a PICO_HOSTNAME se objeví přímo v programu. Může za to příkaz target_compile_definitions v souboru arguments.cmake a makro preprocesoru TO_STRING().

Jediná nectnost tohoto řešení je, že nesmíte mít v některé proměnné znak středník ;. To preprocesor neskousne. Já ho mám zrovna v hesle k wifi, tak jsem ho tam musel doplnit hnusným hackem.

Hnusný hack
setup(/* ... */ TO_STRING(WIFI_PASSWORD)";", auth, /* ... */);
//                                      ^
//                                      |
//                    tohle je ten hack |

Hack spočívá v tom, že C kompilátor spojí dva řetězce za sebou "jeden" "druhy" v jeden jediný řetězec "jedendruhy".

Důležité volby v lwipopts.h:

#define LWIP_IPV6 1
#define LWIP_IPV6_AUTOCONFIG 1
#define LWIP_ND6_RDNSS_MAX_DNS_SERVERS 1        (1)
1 Bez tohoto nefunguje DNS.
CMakeLists.txt
cmake_minimum_required(VERSION 3.13)

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

set(PROJECT SNTP_client)
project(${PROJECT} C CXX ASM)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

pico_sdk_init()

add_executable(${PROJECT}
    main.c
)

target_include_directories(${PROJECT} PRIVATE ${CMAKE_CURRENT_LIST_DIR})

include(arguments.cmake)

target_link_libraries( ${PROJECT}
    pico_stdlib
    pico_cyw43_arch_lwip_threadsafe_background
    hardware_rtc
)

pico_enable_stdio_usb(${PROJECT} 1)
pico_enable_stdio_uart(${PROJECT} 0)
pico_add_extra_outputs(${PROJECT})

Definice argumentů, které při sestavení programu předáváme do C kódu:

arguments.cmake
if (DEFINED ENV{WIFI_SSID} AND (NOT WIFI_SSID))
    set(WIFI_SSID $ENV{WIFI_SSID})
    message("Using WIFI_SSID from environment ('${WIFI_SSID}')")
endif()
if (NOT WIFI_SSID)
    message(FATAL_ERROR "Missing argument WIFI_SSID")
endif()

if (DEFINED ENV{WIFI_PASSWORD} AND (NOT WIFI_PASSWORD))
    set(WIFI_PASSWORD $ENV{WIFI_PASSWORD})
    message("Using WIFI_PASSWORD from environment ('${WIFI_PASSWORD}')")
endif()
if (NOT WIFI_PASSWORD)
    message(FATAL_ERROR "Missing argument WIFI_PASSWORD")
endif()

if (DEFINED ENV{PICO_HOSTNAME} AND (NOT PICO_HOSTNAME))
    set(PICO_HOSTNAME $ENV{PICO_HOSTNAME})
    message("Using PICO_HOSTNAME from environment ('${PICO_HOSTNAME}')")
endif()
if (NOT PICO_HOSTNAME)
    message(FATAL_ERROR "Missing argument PICO_HOSTNAME")
endif()

if (DEFINED ENV{NTP_SERVER} AND (NOT NTP_SERVER))
    set(NTP_SERVER $ENV{NTP_SERVER})
    message("Using NTP_SERVER from environment ('${NTP_SERVER}')")
endif()
if (NOT NTP_SERVER)
    message(FATAL_ERROR "Missing argument NTP_SERVER")
endif()

if (DEFINED ENV{WIFI_DOMA} AND (NOT WIFI_DOMA))
    set(WIFI_DOMA $ENV{WIFI_DOMA})
    message("Mam WIFI_DOMA from environment ('${WIFI_DOMA}')")
endif()
#if (NOT NTP_SERVER)
#    message(FATAL_ERROR "Missing argument NTP_SERVER")
#endif()


target_compile_definitions(${PROJECT} PRIVATE
         WIFI_SSID=${WIFI_SSID}
         WIFI_PASSWORD=${WIFI_PASSWORD}
         PICO_HOSTNAME=${PICO_HOSTNAME}
         NTP_SERVER=${NTP_SERVER}
         WIFI_DOMA=${WIFI_DOMA}
        )

V C kódu je máme zde:

argument_definitions.h
#ifndef WIFI_SSID
#define WIFI_SSID
#endif

#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD
#endif

#ifndef PICO_HOSTNAME
#define PICO_HOSTNAME
#endif

#ifndef NTP_SERVER
#define NTP_SERVER
#endif

#ifndef WIFI_DOMA
#define WIFI_DOMA 0
#endif

Makra preprocesoru, co z argumentů udělají řetězce (použije se ve funkci connect():

// předávání parametrů z cmake
#define STRINGIFY(x) #x
#define TO_STRING(x) STRINGIFY(x)
main.c
/* SNTP klient po IPv6
 * nastavení hodin reálného času po startu systému
 * (c) Jirka Chráska 2025; <jirka@lixis.cz
 */


#include <stdio.h>

#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include "lwip/pbuf.h"
#include "lwip/ip6_addr.h"
#include "lwip/udp.h"
#include "lwip/dns.h"
#include "setupWifi.h"
#include "hardware/rtc.h"
#include "time.h"

#include "argument_definitions.h"

// předávání parametrů z cmake
#define xstr(x) str(x)
#define str(x) #x


// nastavení časové zóny Europe/Prague
// středoevropský čas (v létě to nebude fungovat)
#define TIMEZONE 3600


// připojení k AP
int connect()
{
    uint32_t country = CYW43_COUNTRY_CZECH_REPUBLIC;
    uint32_t auth    = CYW43_AUTH_WPA2_MIXED_PSK;
#if WIFI_DOMA
    printf("Hack (jsem doma): hostname='%s', ssid='%s', password='%s'\n", xstr(PICO_HOSTNAME), xstr(WIFI_SSID), xstr(WIFI_PASSWORD)";");
    // používá se nastavení pomocí DHCP
    return  setup(country, xstr(WIFI_SSID), xstr(WIFI_PASSWORD)";", auth, xstr(PICO_HOSTNAME));
#else 
    printf("hostname='%s', ssid='%s', password='%s'\n", xstr(PICO_HOSTNAME), xstr(WIFI_SSID), xstr(WIFI_PASSWORD));
    // používá se nastavení pomocí DHCP
    return  setup(country, xstr(WIFI_SSID), xstr(WIFI_PASSWORD), auth, xstr(PICO_HOSTNAME));  
#endif
}

// získání času z hodin Pica
bool getDateNow(struct tm *t)
{
    datetime_t rtc;
    bool state = rtc_get_datetime(&rtc);
    if (state)
    {

        t->tm_sec = rtc.sec;
        t->tm_min = rtc.min;
        t->tm_hour = rtc.hour;
        t->tm_mday = rtc.day;
        t->tm_mon = rtc.month - 1;
        t->tm_year = rtc.year - 1900;
        t->tm_wday = rtc.dotw;
        t->tm_yday = 0;
        t->tm_isdst = -1;
    }
    return state;
}

// nastavení hodin reálného času na Picu
void setRTC(struct tm *datetime)
{
    datetime_t t;
    t.year = datetime->tm_year + 1900;
    t.month = datetime->tm_mon + 1;
    t.day = datetime->tm_mday;
    t.dotw = datetime->tm_wday;
    t.hour = datetime->tm_hour;
    t.min = datetime->tm_min;
    t.sec = datetime->tm_sec;
    rtc_init();
    rtc_set_datetime(&t);
}

struct timeStatus
{
    bool ready;
    struct udp_pcb *pcb;
};

void recv(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port)
{
    struct timeStatus *tstatus = (struct timeStatus *)arg;
    printf("SNTP odpovídá\n");
    if (p != NULL)
    {
        uint8_t seconds_buf[4];
        pbuf_copy_partial(p, seconds_buf, sizeof(seconds_buf), 40);
        uint32_t seconds_since_1900 = seconds_buf[0] << 24 | seconds_buf[1] << 16 | seconds_buf[2] << 8 | seconds_buf[3];
        time_t seconds_since_1970 = seconds_since_1900 - 2208988800 + TIMEZONE;
        struct tm *datetime = gmtime(&seconds_since_1970);
        setRTC(datetime);
        pbuf_free(p);
        udp_remove(pcb);
        tstatus->pcb=NULL;
        tstatus->ready = true;
    }
}

bool pollSNTP(struct timeStatus *tstatus)
{
    if (tstatus == NULL)
        return true;
    if (tstatus->ready)
    {
        free(tstatus);
        tstatus = NULL;
        return true;
    }
    return false;
}

void dns_found(const char *name, const ip6_addr_t *ip, void *arg)
{
    struct timeStatus *tstatus = (struct timeStatus *)arg;
    printf("IP NTP serveru z DNS %s\n", ip6addr_ntoa(ip));
    struct udp_pcb *pcb = udp_new_ip_type(6);
    tstatus->pcb = pcb;
    udp_recv(pcb, recv, arg);
    udp_bind(pcb, IP_ADDR_ANY, 123);
    udp_connect(pcb, ip, 123);
    struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, 48, PBUF_RAM);
    uint8_t *payload = (uint8_t *)p->payload;
    memset(payload, 0, 48);
    *payload = 0x1b;
    err_t er = udp_send(pcb, p);
    pbuf_free(p);
}

struct timeStatus *getSNTP()
{
    struct timeStatus *tstatus = malloc(sizeof(struct timeStatus));
    tstatus->ready = false;
    tstatus->pcb=NULL;
    ip6_addr_t ip;
    cyw43_arch_lwip_begin();
    // err_t err = dns_gethostbyname(xstr(NTP_SERVER), &ip, dns_found, tstatus);
    err_t err = dns_gethostbyname_addrtype(xstr(NTP_SERVER), &ip, dns_found, tstatus, LWIP_DNS_ADDRTYPE_IPV6);
    cyw43_arch_lwip_end();
    if (err == ERR_OK)
    {
        printf("DNS %s\n", ip6addr_ntoa(&ip));
        dns_found("", &ip, tstatus);
    } else {
        printf("DNS %s err=%d\n", ip6addr_ntoa(&ip), err);
        
    }
    return tstatus;
}

void cancelSNTP(struct timeStatus *tstatus)
{
    if (tstatus != NULL)
    {
        udp_remove(tstatus->pcb);
        free(tstatus);
        tstatus = NULL;
        printf("zrušeno\n");
    }
}

int main()
{
int connection_status = 0;

    stdio_init_all();
    sleep_ms(2000);
    printf("\nSNTP klient IPv6\n");

    do {
        printf("Připojuji se k AP.\n");
        connection_status = connect();
    } while ( connection_status != CYW43_LINK_UP);
    
    printf("Připojeno.\n");
    printf("Linková IPv6 adresa:  %s\n", ip6addr_ntoa(netif_ip6_addr(netif_default,0)));
    printf("Globální IPv6 adresa: %s\n", ip6addr_ntoa(netif_ip6_addr(netif_default,1)));
    printf("Host Name: %s\n", netif_get_hostname(netif_default));
    
    // nechce-li chodit automatické nastavení DNS serveru
    // dá se nastavit ručně
    //    ip6_addr_t dnsserver_ip ;
    //    ip6addr_aton("2a0e:5340:4:1::1", &dnsserver_ip);
    //    IP_SET_TYPE_VAL(&dnsserver_ip, IPADDR_TYPE_V6);
    //    dns_setserver(0,&dnsserver_ip);
    
    const ip6_addr_t *dnsip = dns_getserver(0);
    printf("DNS server:           %s\n", ip6addr_ntoa(dnsip));
    
    while (true)
    {
        struct timeStatus *tstatus = getSNTP();
        sleep_ms(500);
        if (pollSNTP(tstatus))
            break;
        cancelSNTP(tstatus);
    }

    while (true)
    {
        struct tm t;
        getDateNow(&t);
        char Date[100];
        strftime(Date, sizeof(Date), "Čas: %a, %d %b %Y %k:%M:%S %Z\r", &t);
        printf("%s", Date);
        sleep_ms(1000);
    }
}
setupWifi.h
/* Nastavení Wifi karty */

int setup(uint32_t country, const char *ssid, const char *pass, uint32_t auth, const char *hostname)
{

    if (cyw43_arch_init_with_country(country))
    {
        return 1;
    }

    cyw43_arch_enable_sta_mode();
    if (hostname != NULL)
    {
        netif_set_hostname(netif_default, hostname);
    }
    if (cyw43_arch_wifi_connect_async(ssid, pass, auth))
    {
        return 2;
    }
    int flashrate = 1000;
    int status = CYW43_LINK_UP + 1;
    while (status >= 0 && status != CYW43_LINK_UP)
    {
        int new_status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA);
        if (new_status != status)
        {
            status = new_status;
            flashrate = flashrate / (status + 1);
            printf("Stav připojení: %d %d\n", status, flashrate);
        }
        cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1);
        sleep_ms(flashrate);
        cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0);
        sleep_ms(flashrate);
    }
    if (status < 0)
    {
        cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0);
    }
    else
    {
        cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1);
        
    }
    return status;
}
lwipopts.h
#ifndef _LWIPOPTS_EXAMPLE_COMMONH_H
#define _LWIPOPTS_EXAMPLE_COMMONH_H

// Common settings used in most of the pico_w examples
// (see https://www.nongnu.org/lwip/2_1_x/group__lwip__opts.html for details)

// allow override in some examples
#ifndef NO_SYS
#define NO_SYS 1
#endif
// allow override in some examples
#ifndef LWIP_SOCKET
#define LWIP_SOCKET 0
#endif
#if PICO_CYW43_ARCH_POLL
#define MEM_LIBC_MALLOC 1
#else
// MEM_LIBC_MALLOC is incompatible with non polling versions
#define MEM_LIBC_MALLOC 0
#endif
#define MEM_ALIGNMENT 4
#define MEM_SIZE 4000
#define MEMP_NUM_TCP_SEG 32
#define MEMP_NUM_ARP_QUEUE 10
#define PBUF_POOL_SIZE 24
#define LWIP_ARP 1
#define LWIP_ETHERNET 1
#define LWIP_ICMP 1
#define LWIP_RAW 1
#define TCP_WND (8 * TCP_MSS)
#define TCP_MSS 1460
#define TCP_SND_BUF (8 * TCP_MSS)
#define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS))
#define LWIP_NETIF_STATUS_CALLBACK 1
#define LWIP_NETIF_LINK_CALLBACK 1
#define LWIP_NETIF_HOSTNAME 1
#define LWIP_NETCONN 0
#define MEM_STATS 0
#define SYS_STATS 0
#define MEMP_STATS 0
#define LINK_STATS 0
// #define ETH_PAD_SIZE 2
#define LWIP_CHKSUM_ALGORITHM 3
#define LWIP_DHCP 0
#define LWIP_IPV4 0
#define LWIP_IPV6 1
#define LWIP_IPV6_AUTOCONFIG 1
#define LWIP_ND6_RDNSS_MAX_DNS_SERVERS 1
#define LWIP_TCP 0
#define LWIP_UDP 1
#define LWIP_DNS 1
#define LWIP_TCP_KEEPALIVE 0
#define LWIP_NETIF_TX_SINGLE_PBUF 1
#define DHCP_DOES_ARP_CHECK 0
#define LWIP_DHCP_DOES_ACD_CHECK 0
#define DNS_TABLE_SIZE 1

#ifndef NDEBUG
#define LWIP_DEBUG 1
#define LWIP_STATS 1
#define LWIP_STATS_DISPLAY 1
#endif

#define ETHARP_DEBUG        LWIP_DBG_OFF
#define NETIF_DEBUG         LWIP_DBG_OFF
#define PBUF_DEBUG          LWIP_DBG_OFF
#define API_LIB_DEBUG       LWIP_DBG_OFF
#define API_MSG_DEBUG       LWIP_DBG_OFF
#define SOCKETS_DEBUG       LWIP_DBG_OFF
#define ICMP_DEBUG          LWIP_DBG_OFF
#define INET_DEBUG          LWIP_DBG_OFF
#define IP_DEBUG            LWIP_DBG_OFF
#define IP_REASS_DEBUG      LWIP_DBG_OFF
#define RAW_DEBUG           LWIP_DBG_OFF
#define MEM_DEBUG           LWIP_DBG_OFF
#define MEMP_DEBUG          LWIP_DBG_OFF
#define SYS_DEBUG           LWIP_DBG_OFF
#define TCP_DEBUG           LWIP_DBG_OFF
#define TCP_INPUT_DEBUG     LWIP_DBG_OFF
#define TCP_OUTPUT_DEBUG    LWIP_DBG_OFF
#define TCP_RTO_DEBUG       LWIP_DBG_OFF
#define TCP_CWND_DEBUG      LWIP_DBG_OFF
#define TCP_WND_DEBUG       LWIP_DBG_OFF
#define TCP_FR_DEBUG        LWIP_DBG_OFF
#define TCP_QLEN_DEBUG      LWIP_DBG_OFF
#define TCP_RST_DEBUG       LWIP_DBG_OFF
#define UDP_DEBUG           LWIP_DBG_OFF
#define TCPIP_DEBUG         LWIP_DBG_OFF
#define PPP_DEBUG           LWIP_DBG_OFF
#define SLIP_DEBUG          LWIP_DBG_OFF
#define DHCP_DEBUG          LWIP_DBG_OFF

#endif /* __LWIPOPTS_H__ */
Výstup z minicomu
Jsem doma: hostname='picow-hodiny', ssid='chorche', password='xxxxxxxxxxx'
connect status: 1 500
connect status: 2 166
connect status: 3 41
Připojeno.
Linková IPv6 adresa:     FE80::2ACD:C1FF:FE14:8E67
Globální IPv6 adresa:   2A0E:5340:4:1:2ACD:C1FF:FE14:8E67
Host Name: picow-hodiny
DNS server: 2A0E:5340:4:1::1
DNS cache ::FFFF:FFFF:2004:1F4C:1001:CEE4 err=-5
IP of the NTP server from DNS 2001:718:1:1::144:201
SNTP responded
Date: Wed, 12 Nov 2025 21:57:28