diff --git a/README.md b/README.md index f43a7339a..5d4581b80 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,7 @@ App|Description [picow_blink_fast_clock](pico_w/wifi/blink) | Blinks the on-board LED (which is connected via the WiFi chip) with a faster system clock to show how to reconfigure communication with the WiFi chip at build time under those circumstances. [picow_iperf_server](pico_w/wifi/iperf) | Runs an "iperf" server for WiFi speed testing. [picow_ntp_client](pico_w/wifi/ntp_client) | Connects to an NTP server to fetch and display the current time. +[picow_ntp_system_time](pico_w/wifi/ntp_system_time) | Creates a background time-of-day clock that periodically updates itself from a pool of NTP servers, and uses it to display local time. [picow_tcp_client](pico_w/wifi/tcp_client) | A simple TCP client. You can run [python_test_tcp_server.py](pico_w/wifi/python_test_tcp/python_test_tcp_server.py) for it to connect to. [picow_tcp_server](pico_w/wifi/tcp_server) | A simple TCP server. You can use [python_test_tcp_client.py](pico_w//wifi/python_test_tcp/python_test_tcp_client.py) to connect to it. [picow_tls_client](pico_w/wifi/tls_client) | Demonstrates how to make a HTTPS request using TLS. diff --git a/pico_w/wifi/CMakeLists.txt b/pico_w/wifi/CMakeLists.txt index b096d2583..d38b450be 100644 --- a/pico_w/wifi/CMakeLists.txt +++ b/pico_w/wifi/CMakeLists.txt @@ -14,6 +14,7 @@ else() add_subdirectory_exclude_platforms(httpd) add_subdirectory_exclude_platforms(iperf) add_subdirectory_exclude_platforms(ntp_client) + add_subdirectory_exclude_platforms(ntp_system_time) add_subdirectory_exclude_platforms(tcp_client) add_subdirectory_exclude_platforms(tcp_server) add_subdirectory_exclude_platforms(udp_beacon) diff --git a/pico_w/wifi/ntp_system_time/CMakeLists.txt b/pico_w/wifi/ntp_system_time/CMakeLists.txt new file mode 100644 index 000000000..e48815d57 --- /dev/null +++ b/pico_w/wifi/ntp_system_time/CMakeLists.txt @@ -0,0 +1,20 @@ +add_executable(picow_ntp_system_time + ntp_system_time.c + ) +target_compile_definitions(picow_ntp_system_time PRIVATE + WIFI_SSID=\"${WIFI_SSID}\" + WIFI_PASSWORD=\"${WIFI_PASSWORD}\" + ) +target_include_directories(picow_ntp_system_time PRIVATE + ${CMAKE_CURRENT_LIST_DIR} + ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts +) +target_link_libraries(picow_ntp_system_time + pico_cyw43_arch_lwip_threadsafe_background + pico_lwip_sntp # LWIP sntp application + pico_aon_timer # high-level API for "always on" timer + pico_sync # thread synchronisation (mutex) + pico_stdlib + ) + +pico_add_extra_outputs(picow_ntp_system_time) \ No newline at end of file diff --git a/pico_w/wifi/ntp_system_time/README.md b/pico_w/wifi/ntp_system_time/README.md new file mode 100644 index 000000000..22e952725 --- /dev/null +++ b/pico_w/wifi/ntp_system_time/README.md @@ -0,0 +1,92 @@ +# Overview + +Creates a time of day clock that periodically synchronises itself to Internet time servers using simple NTP (see [RFC 4330](https://datatracker.ietf.org/doc/html/rfc4330)). + +The example connects to Wi-Fi and displays the local time in the UK or another timezone that you specify, synchronised once an hour to one of the servers from [pool.ntp.org](https://www.ntppool.org/en/). + +Uses the SNTP application provided by lwIP and the Pico 'always-on timer' _(RTC on Pico/RP2040, powman timer on Pico-2/RP2350)_. + +# Running the example + +Provide the SSID and password of your Wi-Fi network by editing `CMakeLists.txt` or on the command line; then build and run the example as usual. + +You should see something like this: + +``` +Connecting to Wi-Fi... +connect status: joining +connect status: link up +Connected +IP address 192.168.0.100 +system time not yet initialised +-> initialised system time from NTP +GMT: Sun Oct 26 10:41:07 2025 +GMT: Sun Oct 26 10:41:12 2025 +... +``` + +### To use it in your own code +Configure the lwIP callbacks and connect to the network as shown in the example. You can then initialise the background NTP synchronisation like this: + +``` +sntp_setoperatingmode(SNTP_OPMODE_POLL); +sntp_init(); +``` + +Your code can now call + +``` +void get_time_utc(struct timespec *) +``` + +whenever it wants the current UTC time. + + You can also use the [pico_aon_timer API](https://www.raspberrypi.com/documentation/pico-sdk/high_level.html#group_pico_aon_timer) to read the time directly or to set alarms. _Note however that with a direct read it is theoretically possible (although very unlikely) to get an erroneous result if NTP was in the process of updating the timer in the background._ + + To reduce the logging level change the `SNTP_DEBUG` option in **lwipopts.h** to `LWIP_DBG_OFF` and/or remove the informational messages from `sntp_set_system_time_us()` in **ntp_system_time.c**. + + +# Further details + +The example uses: + +1. the [SNTP application](https://www.nongnu.org/lwip/2_0_x/group__sntp.html) provided by the lwIP network stack +2. the Pico SDK high level "always on timer" abstraction: [pico_aon_timer](https://www.raspberrypi.com/documentation/pico-sdk/high_level.html#group_pico_aon_timer) +3. an optional [POSIX timezone](https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_node/libc_431.html) to convert UTC to local time + +### lwIP SNTP + +The lwIP SNTP app provides a straightforward way to obtain and process timestamps from a pool of NTP servers without the complexity of a full NTP implementation. The lwIP documentation covers the [configuration options](https://www.nongnu.org/lwip/2_0_x/group__sntp.html) but the comments in the [source code](https://github.com/lwip-tcpip/lwip/blob/master/src/apps/sntp/sntp.c) are also very helpful. + +SNTP uses the **macros** `SNTP_GET_SYSTEM_TIME(sec, us)` and `SNTP_SET_SYSTEM_TIME_US(sec, us)` to call user-provided functions for accessing the system clock. The example defines the macros in `lwipopts.h` and the callbacks themselves are near the top of `ntp_system_time.c`. + +Note that the example runs lwIP/SNTP from `pico_cyw43_arch` in _threadsafe background mode_ as described in [SDK Networking](https://www.raspberrypi.com/documentation/pico-sdk/networking.html#group_pico_cyw43_arch). +If you reconfigure it to use _polling mode_ then your user code should periodically call `cyw43_arch_poll()`. + +### Always on timer + +The SDK provides the high level [pico_aon_timer](https://www.raspberrypi.com/documentation/pico-sdk/high_level.html#group_pico_aon_timer) API to provide the same always-on timer functions on Pico and Pico-2 despite their hardware differences. + +On the original Pico (RP2040) these functions use the real time clock (RTC) and on the Pico-2 (RP2350) the POWMAN timer. + +For further details refer to the [SDK documentation](https://www.raspberrypi.com/documentation/pico-sdk/high_level.html#group_pico_aon_timer). + +### POSIX timezone + +NTP timestamps always refer to universal coordinated time (UTC) in seconds past the epoch. In contrast users and user applications often require **local time**, which varies from region to region and at different times of the year (daylight-saving time or DST). + +Converting from UTC to local time often requires inconvenient rules, but fortunately the Pico SDK time-conversion functions like `ctime()` and `pico_localtime_r()` can do it automatically if you define a **POSIX timezone (TZ)**. + +The example shows a suitable definition for the Europe/London timezone: +``` +setenv("TZ", "BST0GMT,M3.5.0/1,M10.5.0/2", 1); +``` + +which means +``` +Normal time ("GMT") is UTC +0. Daylight-saving time ("BST") runs from 1am on the last Sunday in March to 2am on the last Sunday in October. +``` + +The format to define your own POSIX timezone is pretty straightforward and can be found [here](https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_node/libc_431.html); or you can simply choose a pre-defined one from an online resource such as [this](https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv). + +_Note that it is entirely optional to create a local timezone: without one the Pico SDK time-conversion functions will simply use UTC._ \ No newline at end of file diff --git a/pico_w/wifi/ntp_system_time/lwipopts.h b/pico_w/wifi/ntp_system_time/lwipopts.h new file mode 100644 index 000000000..a1cdf11c6 --- /dev/null +++ b/pico_w/wifi/ntp_system_time/lwipopts.h @@ -0,0 +1,43 @@ +#ifndef _LWIPOPTS_H +#define _LWIPOPTS_H + +// Extra options for the lwIP/SNTP application +// (see https://www.nongnu.org/lwip/2_1_x/group__sntp__opts.html) +// +// This example uses a common include to avoid repetition +#include "lwipopts_examples_common.h" + +// If we use SNTP we should increase the number of LWIP system timeouts by one +#define MEMP_NUM_SYS_TIMEOUT (LWIP_NUM_SYS_TIMEOUT_INTERNAL+1) +#define SNTP_MAX_SERVERS LWIP_DHCP_MAX_NTP_SERVERS +#define SNTP_GET_SERVERS_FROM_DHCP LWIP_DHCP_GET_NTP_SRV +#define SNTP_SERVER_DNS 1 +#define SNTP_SERVER_ADDRESS "pool.ntp.org" +// show debug information from the lwIP/SNTP application +#define SNTP_DEBUG LWIP_DBG_ON +#define SNTP_PORT LWIP_IANA_PORT_SNTP +// verify IP addresses and port numbers of received packets +#define SNTP_CHECK_RESPONSE 2 +// compensate for packet transmission delay +#define SNTP_COMP_ROUNDTRIP 1 +#define SNTP_STARTUP_DELAY 1 +#define SNTP_STARTUP_DELAY_FUNC (LWIP_RAND() % 5000) +#define SNTP_RECV_TIMEOUT 15000 +// how often to query the NTP servers, in ms (60000 is the minimum permitted by RFC4330) +#define SNTP_UPDATE_DELAY 3600000 +#define SNTP_RETRY_TIMEOUT SNTP_RECV_TIMEOUT +#define SNTP_RETRY_TIMEOUT_MAX (SNTP_RETRY_TIMEOUT * 10) +#define SNTP_RETRY_TIMEOUT_EXP 1 +#define SNTP_MONITOR_SERVER_REACHABILITY 1 + +//* configure SNTP to use our callback functions for reading and setting the system time +#define SNTP_GET_SYSTEM_TIME(sec, us) sntp_get_system_time_us(&(sec), &(us)) +#define SNTP_SET_SYSTEM_TIME_US(sec, us) sntp_set_system_time_us(sec, us) + +//* declare our callback functions (the implementations are in ntp_system_time.c) +#include "stdint.h" +void sntp_set_system_time_us(uint32_t sec, uint32_t us); +void sntp_get_system_time_us(uint32_t *sec_ptr, uint32_t *us_ptr); + + +#endif /* __LWIPOPTS_H__ */ \ No newline at end of file diff --git a/pico_w/wifi/ntp_system_time/ntp_system_time.c b/pico_w/wifi/ntp_system_time/ntp_system_time.c new file mode 100644 index 000000000..aaddb9eeb --- /dev/null +++ b/pico_w/wifi/ntp_system_time/ntp_system_time.c @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2025 mjcross + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include // needed for setenv(), although also included by lwIP +#include "pico/stdlib.h" +#include "pico/cyw43_arch.h" +#include "lwip/apps/sntp.h" +#include "pico/util/datetime.h" +#include "pico/aon_timer.h" +#include "pico/mutex.h" + +// create a mutex to avoid reading the aon_timer at the same time as lwIP/SNTP is updating it +auto_init_mutex(aon_timer_mutex); +static bool aon_timer_is_initialised = false; + +// callback for lwIP/SNTP to set the aon_timer to UTC +// we configure SNTP to call this function when it receives a valid NTP timestamp +// (see lwipopts.h) +void sntp_set_system_time_us(uint32_t sec, uint32_t us) { + static struct timespec ntp_ts; + ntp_ts.tv_sec = sec; + ntp_ts.tv_nsec = us * 1000; + + if (aon_timer_is_initialised) { + // wait up to 10ms to obtain exclusive access to the aon_timer + if (mutex_enter_timeout_ms (&aon_timer_mutex, 10)) { + aon_timer_set_time(&ntp_ts); + mutex_exit(&aon_timer_mutex); // release the mutex as soon as possible + puts("-> updated system time from NTP"); + } else { + puts("-> skipped NTP system time update (aon_timer was busy)"); + } + } else { + // the aon_timer is uninitialised so we don't need exclusive access + aon_timer_is_initialised = aon_timer_start(&ntp_ts); + puts("-> initialised system time from NTP"); + } +} + +// callback for lwIP/SNTP to read system time (UTC) from the aon_timer +// we configure SNTP to call this function to read the current UTC system time, +// eg to calculate the roundtrip transmission delay (see lwipopts.h) +void sntp_get_system_time_us(uint32_t *sec_ptr, uint32_t * us_ptr) { + static struct timespec sys_ts; + // we don't need exclusive access because we are on the background thread + aon_timer_get_time(&sys_ts); + *sec_ptr = sys_ts.tv_sec; + *us_ptr = sys_ts.tv_nsec / 1000; +} + +// function for user code to safely read the system time (UTC) asynchronously +int get_time_utc(struct timespec *ts_ptr) { + int retval = 1; + if (mutex_enter_timeout_ms(&aon_timer_mutex, 10)) { + aon_timer_get_time(ts_ptr); + mutex_exit(&aon_timer_mutex); + retval = 0; + } + return retval; +} + +int main() { + stdio_init_all(); + + // Initialise the Wi-Fi chip + if (cyw43_arch_init()) { + printf("Wi-Fi init failed\n"); + return -1; + } + + // Enable wifi station mode + cyw43_arch_enable_sta_mode(); + printf("Connecting to Wi-Fi...\n"); + if (cyw43_arch_wifi_connect_timeout_ms(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK, 30000)) { + printf("failed to connect\n"); + return 1; + } + + // display the ip address in human readable form + uint8_t *ip_address = (uint8_t*)&(netif_default->ip_addr.addr); + printf("IP address %d.%d.%d.%d\n", ip_address[0], ip_address[1], ip_address[2], ip_address[3]); + + // initialise the lwIP/SNTP application + sntp_setoperatingmode(SNTP_OPMODE_POLL); // lwIP/SNTP also accepts SNTP_OPMODE_LISTENONLY + sntp_init(); + + + // ----- simple demonstration of how to read and display the system time ----- + // + struct timespec ts; + struct tm tm; + + // OPTIONAL: set the 'TZ' env variable to the local POSIX timezone (in this case Europe/London) + // For the format see: https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_node/libc_431.html + // or just copy one from (eg): https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv + setenv("TZ", "BST0GMT,M3.5.0/1,M10.5.0/2", 1); + + // If the environment contains a valid 'TZ' definition then functions like ctime(), localtime() + // and their variants automatically give results converted to the local timezone instead of UTC + // (see below). + + while (true) { + + if(aon_timer_is_initialised) { + + // safely read the current time as UTC seconds and ms since the epoch + get_time_utc(&ts); + + // if you don't need the date/time fields you can call `ctime()` or one of its variants + // here to convert the UTC seconds count to a string like "Mon Oct 27 22:06:08 2025\n". + // If you have defined a valid 'TZ' the string will be in local time, otherwise UTC. + //printf("%s", ctime(&(ts.tv_sec))); + + // you can extract the date/time fields using `localtime()` or one of its variants. If you + // have defined a valid 'TZ' then the field values will be in local time, otherwise UTC. + pico_localtime_r(&(ts.tv_sec), &tm); + + // display the name of the currently active local timeszone, if defined + if (getenv("TZ")) { + printf("%s: ", tm.tm_isdst ? tzname[0]: tzname[1]); + // defines `extern char *tzname[2]` to hold the names of the POSIX timezones + } else { + printf("UTC: "); + } + + // you can use `asctime()` and its variants to convert the date/time fields into a string + // like: "Mon Oct 27 22:06:08 2025\n". If you need more flexibility consider `strftime()` + printf("%s", asctime(&tm)); + + } else { + puts("system time not yet initialised"); + } + + sleep_ms(5000); // do nothing for 5 seconds + } +}