Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}