Zephyr tutorial 201 – Zephyr in the age of Connectivity

Zephyr tutorial 201 – Zephyr in the age of Connectivity

Javad Rahimipetroudi (Mind embedded software consultant)
Javad Rahamipetroudi
22/05/2024

Introduction

In the previous Zephyr tutorials, we developed our basic understanding of Zephyr RTOS and got hands-on experience with it. In this tutorial series, we are going to get familiar with the TCP/IP networking stack in Zephyr. As usual, we will do it by practical examples, such that each of them will be a complete solution for our daily problems.

As the number of topics is too much that be listed, we are going to develop a tree-like structure to organize the tutorials in a traceable manner. The following diagram depicts our journey through the Zephyr network stack. We will traverse the tree from left to right (aka Preorder in Data structure).

As the root of the tree, you will find this tutorial. In this tutorial, we will develop a very simple demo network application in Zephyr.

Then, from left to right, we will study existing popular Application protocols in Zephyr that can be time-saving for us. As each network client will need a server or at least a peer, we will also learn how to set up the required infrastructure.

We will dirty our hands in the socket tutorials by directly using the available socket APIs in Zephyr. Finally, by continuing our traverse, we will reach the most important part of our tutorials, using the power of connectivity in Zephyr to port our application with different technologies.

Prerequisites

As we are going to work with a network stack, we need a board with network connectivity. We are going to use the STMicroelectronics NUCLEO-H732ZG board. This board has an Ethernet port connected to the microcontroller. However, the concept is the same for the development boards with WiFi connectivity, except that connecting to WiFi is a bit more complicated since you need to set SSID and password.

This tutorial starts out using that board to get a general feeling of how network programming looks in Zephyr. For this purpose, we will develop a simple SNTP (Simple Network Time Protocol) client that reads Unix time (Epoch) time from a NTP server and displays it in the debug console.

Since this is a more in-depth Zephyr tutorial series, we assume that you’ll be able by yourself to find out how to configure Zephyr for your board and how to run the west tool to build and flash the application.

A simple SNTP demo

As the first step, let’s create a project for SNTP client demo. There is a sntp_client application in the samples directory of Zephyr. However, we will start from scratch to have a better understanding of the process. Furthermore, to make it more practical, we will update the internal RTC with the received data. You can find the source code of this demo on gitlab. The purpose of this demo is a high-level overview of the network programming in Zephyr by answering the following questions:

  • Are we going to be faced with strange APIs?
  • Is there a lot of low-level configuration per project?

SNTP Protocol

The NTP protocol is used to synchronize computer clocks on the global Internet. The full protocol, however, leverages complex algorithms to get the most accurate estimate of the current time in the face of unpredictable network latencies. For low-power microcontrollers, this is usually considered too expensive and simply unnecessary. Therefore, SNTP (Simple Network Time Protocol) was developed to be less resource hungry but less accurate than NTP.

On the wire, SNTP is the same as NTP, so it also uses UDP on port 123. Consequently, it can be used with the existing NTP servers across the Internet. The following diagram depicts the (S)NTP timestamp format. According to the diagram, it has two parts with a 4-byte length for each. The first part are the elapsed seconds from 1 January 1900 at 00:00 UTC, and the second part is the seconds fraction in units of 2-32s. As almost all network protocols, the values are encoded big-endian. For more information about SNTP protocols, its RFC is a valuable source.

The full NTP protocol has many fields and multiple timestamps. In SNTP, all of these are set to zero or well-known fixed values, and we only look at the “transmit timestamp” field in the response from the NTP server.

Project configuration file

This is a trimmed version of the Zephyr demo that simply configures the project to work with IPV4.

# General config
CONFIG_REQUIRES_FULL_LIBC=y
CONFIG_POSIX_API=y

# Networking config
CONFIG_NETWORKING=y
CONFIG_NET_IPV4=y
CONFIG_NET_IPV6=n
CONFIG_NET_UDP=y
CONFIG_NET_SHELL=n

# Sockets
CONFIG_NET_SOCKETS=y
CONFIG_NET_SOCKETS_POLL_MAX=4

# Network driver config
CONFIG_TEST_RANDOM_GENERATOR=y

# Network address config
CONFIG_NET_CONFIG_SETTINGS=y
CONFIG_NET_CONFIG_NEED_IPV4=y
CONFIG_NET_CONFIG_NEED_IPV6=n
CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.168.1.15"
CONFIG_NET_CONFIG_MY_IPV4_GW="192.168.1.1"

# SNTP
CONFIG_SNTP=y
CONFIG_MAIN_STACK_SIZE=2048
# Network debug config
CONFIG_NET_LOG=n
CONFIG_LOG=y
CONFIG_NET_SOCKETS_LOG_LEVEL_DBG=y
CONFIG_SNTP_LOG_LEVEL_DBG=y

Project source file

Required header files:

/*
 * Copyright (c) 2024 Javad Rahimipetroudi <javad.rahimipetroudi@mind.be>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include 
#include 
#include <zephyr/kernel.h>
#include <zephyr/net/sntp.h>
#include <arpa/inet.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sntp_demo, LOG_LEVEL_DBG);

Constants:

#define SERVER_PORT	123
#define SERVER_ADDR	"192.168.1.16"

#define TIME_BUF_LEN	80

Get data from NTP:

/**
 * @brief: This method connects to the NTP server and gets the current time
 *
 */
int sntp_get_time(struct sntp_ctx *ctx, const char* server_addr, const unsigned int server_port, time_t * out_time)
{
    int ret = 0;
    struct sntp_time sntp_time;
    struct sockaddr_in addr;

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(SERVER_PORT);

    inet_pton(AF_INET, SERVER_ADDR, &addr.sin_addr);


    ret = sntp_init(ctx, (struct sockaddr *) &addr, sizeof(struct sockaddr_in));
    if(ret < 0)
    {
        LOG_ERR("Failed to initialize SNTP, errno: %d", ret);
        return ret;
    }

    LOG_INF("Sending SNTP IPv4 request...");

    ret = sntp_query(ctx, 10*MSEC_PER_SEC, &sntp_time);
    if (ret < 0) { LOG_ERR("SNTP IPv4 request failed: %d", ret); return ret; } LOG_INF("time since Epoch: high word: %u, low word: %u", (uint32_t)(sntp_time.seconds >> 32), (uint32_t)sntp_time.seconds);
    *out_time = sntp_time.seconds;

    return ret;
}

Note that sntp_query has already converted the seconds timestamp in sntp_time from NTP time (since Jan. 1, 1900) to Unix time (since Jan 1, 1970). It has also collected the high and low words of the timestamp (which are spread out over different fields in the NTP format) into a single 64-bit number.

Convert epoch data to human-readable. Since we have a Unix timestamp, we can use localtime and strftime to convert it to a human-readable timestamp.

/**
 * @brief: This method convers time from epoch to local human 
 * readable time
 *
 */
int epoch_to_datetime(time_t * epoch_time, char *date_time, const size_t len)
{
    int ret = 0;
    struct tm  ts;

    assert(epoch_time != NULL);

    // Format time, "ddd yyyy-mm-dd hh:mm:ss zzz"
    ts = *localtime(epoch_time);
    strftime(date_time, len, "%a %Y-%m-%d %H:%M:%S %Z", &ts);

    return ret;
}

Note how the above code looks exactly like how you would program it under Linux.

Update the internal RTC using Zephyr’s RTC abstraction rtc_set_time:

/**
 * @brief: This method update internal RTC of the microcontroller
 *
 */
int rtc_update_time(const time_t * sntp_time)
{
    int ret = 0;
    struct rtc_time datetime_set;

    gmtime_r(sntp_time, (struct tm *)(&datetime_set));
    ret = rtc_set_time(rtc, &datetime_set);
    return ret;
}

/**
 * @brief: This method fetchs current time from internal RTC
 *
 */
int rtc_get_current_time(struct rtc_time *datetime)
{
    int ret = 0;
    assert(datetime != NULL);

    ret = rtc_get_time(rtc, datetime);
    return ret;
}

Main function:

int main(void)
{
    struct sntp_ctx ctx;
    time_t ntp_time;
    char   time_buf[TIME_BUF_LEN] = {0};
    struct rtc_time timedate_get;
    int ret = 0;

    ret = sntp_get_time(&ctx, SERVER_ADDR, SERVER_PORT, &ntp_time);
    assert(ret == 0);
    
    ret = rtc_update_time(&ntp_time);
    assert(ret == 0);
    while(1)
    {
        rtc_get_current_time(&timedate_get);
        LOG_INF("Time: %d:%d:%d", timedate_get.tm_hour,timedate_get.tm_min,timedate_get.tm_sec);
        k_msleep(DELAY_TIME);
    }
}

Let’s take a look at the output:

Conclusion

In this tutorial, we just tried to show you an overall feeling of the network programming in Zephyr. In the next tutorial we will use the low-level socket APIs (which will look very familiar to anyone who developed a socket application on Unix) to develop some practical applications that can be directly used in embedded projects. As embedded engineers, we learn by doing, It is highly recommended to equip yourself with a development board that supports at least one of the types of connectivity.

Presentations

Drop the docs and embrace the model with Gaphor Fosdem '24 - Frank Van Bever 20 March, 2024 Read more
How to update your Yocto layer for embedded systems? ER '23 -Charles-Antoine Couret 28 September, 2023 Read more
Tracking vulnerabilities with Buildroot & Yocto EOSS23 conference - Arnout Vandecapelle 12 July, 2023 Read more
Lua for the lazy C developer Fosdem '23 - Frank Van Bever 5 February, 2023 Read more
Exploring a Swedish smart home hub Fosdem '23 - Hannah Kiekens 4 February, 2023 Read more
prplMesh An Open-source Implementation of the Wi-Fi Alliance® Multi-AP (Arnout Vandecappelle) 25 October, 2018 Read more

 

News