Skip to content
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. a [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(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 device) these functions use the real time clock (RTC) and on the Pico-2 (rp2350 device) 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 inconventient 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).

_Note that it is entirely optional to create a local timezone: without one the Pico SDK time-conversion functions will simply use UTC._
45 changes: 45 additions & 0 deletions pico_w/wifi/ntp_system_time/lwipopts.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#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 to set the system time
#define SNTP_SET_SYSTEM_TIME_US(sec, us) sntp_set_system_time_us(sec, us)

// declare our callback functions (they are defined 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__ */
119 changes: 119 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,119 @@
/**
* 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 (see lwipopts.h)
// this is called every time the application receives a valid NTP server response
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
// when it needs to (eg) calculate the roundtrip transmission delay
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();

// If you (optionally) define a POSIX TZ here then the example will display local time instead of UTC.
// For the format see https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_node/libc_431.html
setenv("TZ", "BST0GMT,M3.5.0/1,M10.5.0/2", 1); // <-- this is the timezone spec for Europe/London
// there is no need to call tzset()

// 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;

while (true) {

if(aon_timer_is_initialised) {

// read the current time as UTC seconds and ms since the epoch
get_time_utc(&ts);

// if you simply want to display the local time you could now do so with
// puts(ctime(&(ts.tv_sec)));

// to unpack the hours/mins/seconds etc for the local time zone (if defined, see above)
pico_localtime_r(&(ts.tv_sec), &tm); // convert UTC linear time to broken-down local time
printf("%s: %s", tm.tm_isdst ? "BST": "GMT", asctime(&tm)); // display as text

} else {
puts("system time not yet initialised");
}

sleep_ms(5000); // do nothing for 5 seconds
}
}