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.
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__ */
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