Skip to content
Open
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions pico_w/wifi/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions pico_w/wifi/ntp_system_time/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
92 changes: 92 additions & 0 deletions pico_w/wifi/ntp_system_time/README.md
Original file line number Diff line number Diff line change
@@ -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._
47 changes: 47 additions & 0 deletions pico_w/wifi/ntp_system_time/lwipopts.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#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

// configure SNTP to use our callback to read the system time
#define SNTP_GET_SYSTEM_TIME(sec, us) sntp_get_system_time_us(&(sec), &(us))

#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__ */
142 changes: 142 additions & 0 deletions pico_w/wifi/ntp_system_time/ntp_system_time.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* Copyright (c) 2025 mjcross
*
* SPDX-License-Identifier: BSD-3-Clause
*/

#include <stdio.h>
#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 just want a string representation of the current time and you're not interested
// in the individual date/time fields, then here you can simply call:

// if you don't need the date/time fields, you can call `ctime()` or one of its variants
// here to convert the raw timer value into 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 use `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]);
// <time.h> 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
}
}