Maker.io main logo

Introduction to Zephyr Part 11: WiFi and IoT

87

2025-05-15 | By ShawnHymel

ESP32

In this tutorial, we'll walk you through setting up WiFi on a Zephyr-based device and performing basic network operations, including a simple HTTP GET request. The code will demonstrate connecting to a WiFi network, obtaining an IP address, and handling events like connection and disconnection. We'll use Zephyr's WiFi Management API to manage the networking stack, making it easy to port your code to other embedded systems. It provides a foundation for creating Internet of Things (IoT) devices in Zephyr.

You just need the ESP32-S3-DevKitC for this demonstration–no external hardware is required.

All code for this Introduction to Zephyr series can be found here: https://github.com/ShawnHymel/introduction-to-zephyr

Example Application: WiFi and HTTP GET

This application demonstrates how to connect a Zephyr-based embedded device to a WiFi network, perform a DNS lookup, and send an HTTP GET request to a specified server. It initializes WiFi functionality, establishes a connection using the provided SSID and password credentials, and waits for an IP address to be assigned. Once connected, it resolves the server's domain name to an IP address, creates a TCP socket, sends the HTTP GET request, and retrieves the server's response, printing it to the console. The application showcases key Zephyr networking APIs and provides a practical example of integrating networking into embedded systems.

Project Setup

Create a new project directory structure:

Copy Code
/workspace/apps/11_demo_wifi/
├─ boards/
│  ├─ esp32s3_devkitc.conf
│  └─ esp32s3_devkitc.overlay
├─ src/
│  ├─ main.c
│  ├─ wifi.c
│  └─ wifi.h
├─ CMakeLists.txt
└─ prj.conf
 

CMakeLists.txt:

We make a slight change from our normal boilerplate CMakeLists.txt–instead of adding the single main.c file to our app, we need to include our custom wifi.c library as part of the app target. To do that, we perform file globbing, which searches for and creates a list of all the files that match a particular pattern. In this case, we look for any .c files in src/ and store that list in the app_sources variable, which is then passed to the target_sources() function during the build process.

Because we include wifi.h at the top of main.c as a relative path (and wifi.h is in the same directory as main.c), we do not need to call target_include_directories().

Copy Code
cmake_minimum_required(VERSION 3.20.0)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(demo_wifi)

FILE(GLOB app_sources src/*.c)
target_sources(app PRIVATE ${app_sources})
 

prj.conf

We need to enable several software subsystems to configure the WiFi driver, general networking, IPv4 (which we’ll use in this demo), IPv6 (if needed), DHCP, DNS requests, and sockets (for making HTTP requests). These Zephyr systems are required regardless of which hardware platform you are using. Feel free to use menuconfig to learn more about them.

Copy Code
# Remove color codes in log output
CONFIG_LOG_MODE_MINIMAL=y

# Increase stack memory to avoid crashes
CONFIG_MAIN_STACK_SIZE=4096

# Enable WiFi
CONFIG_WIFI=y

# Networking config
CONFIG_NETWORKING=y
CONFIG_NET_IPV4=y
CONFIG_NET_IPV6=y
CONFIG_NET_TCP=y
CONFIG_NET_SOCKETS=y

# Get IPv4 address from DHCP
CONFIG_NET_DHCPV4=y

# Enable DNS resolver
CONFIG_DNS_RESOLVER=y

# How long to wait for e.g. DHCP to provide an IP address (seconds)
CONFIG_NET_CONFIG_INIT_TIMEOUT=30

# Network debug config
CONFIG_NET_LOG=y

# Enable Ethernet (required for WiFi)
CONFIG_NET_L2_ETHERNET=y
 

boards/esp32s3_devkitc.conf

We need to enable some ESP32-specific software components in addition to the ones in prj.conf. To keep our code portable, we’ll put these in a separate Kconfig .conf file that gets pulled in during the build process. Espressif requires us to enable STA_AUTO_DHCP to obtain an IP address from the network’s DHCP server, and they want us to use the heap system with a fairly large memory pool to get WiFi working.

Copy Code
# Required to get an IP address from DHCP
CONFIG_ESP32_WIFI_STA_AUTO_DHCPV4=y

# Use system heap (instead of runtime) for WiFi
CONFIG_ESP_WIFI_HEAP_SYSTEM=y
CONFIG_HEAP_MEM_POOL_SIZE=51200
 

boards/esp32s3_devkitc.overlay

In addition to the software components, we also need to enable the wifi node in Devicetree. The node is defined in zephyr/dts/xtensa/espressif/esp32s3/esp32s3_common.dtsi. All we need to do is change the status from “disabled” to “okay” to enable it.

Copy Code
&wifi {
    status = "okay";
};
 

src/wifi.h

We are going to create a simple wrapper for managing the WiFi connection that uses underlying Zephyr functions. While we forgo some advanced networking, it will keep our main.c simple and clean. This header file acts as a public interface for our main.c application.

Copy Code
#ifndef WIFI_H_
#define WIFI_H_

// Function prototypes
void wifi_init(void);
int wifi_connect(char *ssid, char *psk);
void wifi_wait_for_ip_addr(void);
int wifi_disconnect(void);

#endif // WIFI_H_
 

src/wifi.c

The wifi.c file manages WiFi connectivity on a Zephyr-based device. It initializes WiFi-related event callbacks to handle connection, disconnection, and IP address assignment events. The file defines synchronization mechanisms using semaphores to block execution until key events, such as a successful WiFi connection or IP address assignment, occur. The wifi_connect() function establishes a WiFi connection using SSID and password credentials, while wifi_wait_for_ip_addr() ensures the device has acquired an IP address before proceeding. Additionally, the file includes functionality to query the network interface for details like IP and gateway addresses and handles WiFi disconnection gracefully. By abstracting these tasks, wifi.c simplifies the integration of WiFi capabilities into Zephyr applications.

Copy Code
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/net/wifi_mgmt.h>

// Event callbacks
static struct net_mgmt_event_callback wifi_cb;
static struct net_mgmt_event_callback ipv4_cb;

// Semaphores
static K_SEM_DEFINE(sem_wifi, 0, 1);
static K_SEM_DEFINE(sem_ipv4, 0, 1);

// Called when the WiFi is connected
static void on_wifi_connection_event(struct net_mgmt_event_callback *cb,
                                     uint32_t mgmt_event,
                                     struct net_if *iface)
{
    const struct wifi_status *status = (const struct wifi_status *)cb->info;

    if (mgmt_event == NET_EVENT_WIFI_CONNECT_RESULT) {
        if (status->status) {
            printk("Error (%d): Connection request failed\r\n", status->status);
        } else {
            printk("Connected!\r\n");
            k_sem_give(&sem_wifi);
        }
    } else if (mgmt_event == NET_EVENT_WIFI_DISCONNECT_RESULT) {
        if (status->status) {
            printk("Error (%d): Disconnection request failed\r\n", status->status);
        } else {
            printk("Disconnected\r\n");
            k_sem_take(&sem_wifi, K_NO_WAIT);
        }
    }
}

// Event handler for WiFi management events
static void on_ipv4_obtained(struct net_mgmt_event_callback *cb,
                             uint32_t mgmt_event,
                             struct net_if *iface)
{
    // Signal that the IP address has been obtained
    if (mgmt_event == NET_EVENT_IPV4_ADDR_ADD) {
        k_sem_give(&sem_ipv4);
    }
}

// Initialize the WiFi event callbacks
void wifi_init(void)
{
    // Initialize the event callbacks
    net_mgmt_init_event_callback(&wifi_cb,
                                 on_wifi_connection_event,
                                 NET_EVENT_WIFI_CONNECT_RESULT | NET_EVENT_WIFI_DISCONNECT_RESULT);
    net_mgmt_init_event_callback(&ipv4_cb,
                                 on_ipv4_obtained,
                                 NET_EVENT_IPV4_ADDR_ADD);
    
    // Add the event callbacks
    net_mgmt_add_event_callback(&wifi_cb);
    net_mgmt_add_event_callback(&ipv4_cb);
}

// Connect to the WiFi network (blocking)
int wifi_connect(char *ssid, char *psk)
{
    int ret;
    struct net_if *iface;
    struct wifi_connect_req_params params;

    // Get the default networking interface
    iface = net_if_get_default();

    // Fill in the connection request parameters
    params.ssid = (const uint8_t *)ssid;
    params.ssid_length = strlen(ssid);
    params.psk = (const uint8_t *)psk;
    params.psk_length = strlen(psk);
    params.security = WIFI_SECURITY_TYPE_PSK;
    params.band = WIFI_FREQ_BAND_UNKNOWN;
    params.channel = WIFI_CHANNEL_ANY;
    params.mfp = WIFI_MFP_OPTIONAL;

    // Connect to the WiFi network
    ret = net_mgmt(NET_REQUEST_WIFI_CONNECT,
                   iface,
                   &params,
                   sizeof(params));

    // Wait for the connection to complete
    k_sem_take(&sem_wifi, K_FOREVER);

    return ret;
}

// Wait for IP address (blocking)
void wifi_wait_for_ip_addr(void)
{
    struct wifi_iface_status status;
    struct net_if *iface;
    char ip_addr[NET_IPV4_ADDR_LEN];
    char gw_addr[NET_IPV4_ADDR_LEN];

    // Get interface
    iface = net_if_get_default();

    // Wait for the IPv4 address to be obtained
    k_sem_take(&sem_ipv4, K_FOREVER);

    // Get the WiFi status
    if (net_mgmt(NET_REQUEST_WIFI_IFACE_STATUS,
                 iface,
                 &status,
                 sizeof(struct wifi_iface_status))) {
        printk("Error: WiFi status request failed\r\n");
    }

    // Get the IP address
    memset(ip_addr, 0, sizeof(ip_addr));
    if (net_addr_ntop(AF_INET,
                      &iface->config.ip.ipv4->unicast[0].ipv4.address.in_addr,
                      ip_addr,
                      sizeof(ip_addr)) == NULL) {
        printk("Error: Could not convert IP address to string\r\n");
    }

    // Get the gateway address
    memset(gw_addr, 0, sizeof(gw_addr));
    if (net_addr_ntop(AF_INET,
                      &iface->config.ip.ipv4->gw,
                      gw_addr,
                      sizeof(gw_addr)) == NULL) {
        printk("Error: Could not convert gateway address to string\r\n");
    }

    // Print the WiFi status
    printk("WiFi status:\r\n");
    if (status.state >= WIFI_STATE_ASSOCIATED) {
        printk("  SSID: %-32s\r\n", status.ssid);
        printk("  Band: %s\r\n", wifi_band_txt(status.band));
        printk("  Channel: %d\r\n", status.channel);
        printk("  Security: %s\r\n", wifi_security_txt(status.security));
        printk("  IP address: %s\r\n", ip_addr);
        printk("  Gateway: %s\r\n", gw_addr);
    }
}

// Disconnect from the WiFi network
int wifi_disconnect(void)
{
    int ret;
    struct net_if *iface = net_if_get_default();

    ret = net_mgmt(NET_REQUEST_WIFI_DISCONNECT, iface, NULL, 0);

    return ret;
}
 

src/main.c

Our main application demonstrates how to use the WiFi and networking functionality provided in wifi.c to connect our board to a network and perform an HTTP GET request. It begins by initializing WiFi, connecting to a specified network using the provided SSID and password credentials, and waiting for an IP address to be assigned. Once connected, the program performs a DNS lookup to resolve a domain name into an IP address and establishes a TCP connection to the server using a socket. It then sends an HTTP GET request, receives the server's response in chunks, and prints the response and the total number of bytes received. It is a practical example of integrating networking operations into an embedded application using Zephyr's APIs.

Important! Change MySSID and MyPassword to the SSID and password of your WiFi network.

Zephyr does its best to implement a BSD-style socket API (also known as the “POSIX socket API”), much like you would find when programming networking applications in Linux, macOS, etc. If you enable the CONFIG_NET_SOCKETS Kconfig symbol, you can use the direct BSD functions, like socket(), listen(), recv(), send(), etc. In my experience, these sometimes do not work. If you are OK with slightly different names, you can use the same functions with the zsock_* prefix (as shown in the example below). I found that these work better in Zephyr applications, assuming you do not need cross-BSD (e.g., Linux) functionality for your application.

Copy Code
#include <stdio.h>
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/net/socket.h>

// Custom libraries
#include "wifi.h"

// WiFi settings
#define WIFI_SSID "MySSID"
#define WIFI_PSK "MyPassword"

// HTTP GET settings
#define HTTP_HOST "example.com"
#define HTTP_URL "/"

// Globals
static char response[512];

// Print the results of a DNS lookup
void print_addrinfo(struct zsock_addrinfo **results)
{
    char ipv4[INET_ADDRSTRLEN];
    char ipv6[INET6_ADDRSTRLEN];
    struct sockaddr_in *sa;
    struct sockaddr_in6 *sa6;
    struct zsock_addrinfo *rp;

    // Iterate through the results
    for (rp = *results; rp != NULL; rp = rp->ai_next) {

        // Print IPv4 address
        if (rp->ai_addr->sa_family == AF_INET) {
            sa = (struct sockaddr_in *)rp->ai_addr;
            zsock_inet_ntop(AF_INET, &sa->sin_addr, ipv4, INET_ADDRSTRLEN);
            printk("IPv4: %s\r\n", ipv4);
        }

        // Print IPv6 address
        if (rp->ai_addr->sa_family == AF_INET6) {
            sa6 = (struct sockaddr_in6 *)rp->ai_addr;
            zsock_inet_ntop(AF_INET6, &sa6->sin6_addr, ipv6, INET6_ADDRSTRLEN);
            printk("IPv6: %s\r\n", ipv6);
        }
    }
}

int main(void)
{
    struct zsock_addrinfo hints;
    struct zsock_addrinfo *res;
    char http_request[512];
    int sock;
    int len;
    uint32_t rx_total;
    int ret;

    printk("HTTP GET Demo\r\n");

    // Initialize WiFi
    wifi_init();

    // Connect to the WiFi network (blocking)
    ret = wifi_connect(WIFI_SSID, WIFI_PSK);
    if (ret < 0) {
        printk("Error (%d): WiFi connection failed\r\n", ret);
        return 0;
    }

    // Wait to receive an IP address (blocking)
    wifi_wait_for_ip_addr();

    // Construct HTTP GET request
    snprintf(http_request,
             sizeof(http_request),
             "GET %s HTTP/1.1\r\nHost: %s\r\n\r\n",
             HTTP_URL,
             HTTP_HOST);

    // Clear and set address info
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET;              // IPv4
    hints.ai_socktype = SOCK_STREAM;        // TCP socket

    // Perform DNS lookup
    printk("Performing DNS lookup...\r\n");
    ret = zsock_getaddrinfo(HTTP_HOST, "80", &hints, &res);
    if (ret != 0) {
        printk("Error (%d): Could not perform DNS lookup\r\n", ret);
        return 0;
    }

    // Print the results of the DNS lookup
    print_addrinfo(&res);

    // Create a new socket
    sock = zsock_socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (sock < 0) {
        printk("Error (%d): Could not create socket\r\n", errno);
        return 0;
    }

    // Connect the socket
    ret = zsock_connect(sock, res->ai_addr, res->ai_addrlen);
    if (ret < 0) {
        printk("Error (%d): Could not connect the socket\r\n", errno);
        return 0;
    }

    // Set the request
    printk("Sending HTTP request...\r\n");
    ret = zsock_send(sock, http_request, strlen(http_request), 0);
    if (ret < 0) {
        printk("Error (%d): Could not send request\r\n", errno);
        return 0;
    }

    // Print the response
    printk("Response:\r\n\r\n");
    rx_total = 0;
    while (1) {

        // Receive data from the socket
        len = zsock_recv(sock, response, sizeof(response) - 1, 0);

        // Check for errors
        if (len < 0) {
            printk("Receive error (%d): %s\r\n", errno, strerror(errno));
            return 0;
        }

        // Check for end of data
        if (len == 0) {
            break;
        }

        // Null-terminate the response string and print it
        response[len] = '\0';
        printk("%s", response);
        rx_total += len;
    }

    // Print the total number of bytes received
    printk("\r\nTotal bytes received: %u\r\n", rx_total);

    // Close the socket
    zsock_close(sock);

    return 0;
}

Build and Flash

In the Docker container, build the demo application. Note the additional EXTRA_CONF_FILE parameter to include our ESP32-specific Kconfig settings.

Copy Code
cd /workspace/apps/11_demo_wifi/
west build -p always -b esp32s3_devkitc/esp32s3/procpu -- -DDTC_OVERLAY_FILE=boards/esp32s3_devkitc.overlay -DEXTRA_CONF_FILE=boards/esp32s3_devkitc.conf

On your host computer, flash the application (replace <PORT>  with the USB port for your ESP32-S3-DevKitC):

Copy Code
python -m esptool --port "<PORT>" --chip auto --baud 921600 --before default_reset --after ‎hard_reset write_flash -u --flash_size detect 0x0 ‎workspace/apps/11_demo_wifi/build/zephyr/zephyr.bin ‎

After flashing completes, open a serial port:

Copy Code
python -m serial.tools.miniterm "<PORT>" 115200 ‎

You should see the HTML contents of example.com printed to the terminal.

Introduction to Zephyr Part 11: WiFi and IoT

Challenge: HTTP Client

In addition to the BSD-style sockets, Zephyr implements an HTTP client that handles much of the HTTP request formatting and responses. Your challenge is to use this subsystem (hint: you’ll need to enable a Kconfig symbol) to perform a GET request to example.com and print the response (HTML) instead of using raw sockets. The output should be the same as the previous demo, and you can find my solution here.

Going Further

We explored how to connect a Zephyr-based device to a WiFi network, resolve a domain name, and send an HTTP GET request to a server. We saw how to manage WiFi events, handle IP address assignments, and utilize Zephyr’s networking APIs for robust and reliable communication. This example highlights the versatility of Zephyr in building networked embedded applications, providing a strong foundation for more complex tasks like secure HTTPS communication or integrating IoT protocols.

We only scratched the surface with the networking capabilities in Zephyr, as Zephyr offers a huge range of various underlying networking drivers (e.g., Ethernet and WiFi for supported boards) as well as high-level protocol stacks for UDP, TCP, TLS, MQTT, etc. Here are some recommended articles if you would like to dive deeper:

製造商零件編號 ESP32-S3-DEVKITC-1-N32R8V
ESP32-S3-WROOM-2-N32R8V DEV BRD
Espressif Systems
製造商零件編號 3533
GRAPHIC DISPLAY TFT RGB 0.96"
Adafruit Industries LLC
製造商零件編號 1782
MCP9808 TEMP I2C BREAKOUT BRD
Adafruit Industries LLC
製造商零件編號 3386P-1-103TLF
TRIMMER 10K OHM 0.5W PC PIN TOP
Bourns Inc.
製造商零件編號 1825910-6
SWITCH TACTILE SPST-NO 0.05A 24V
TE Connectivity ALCOSWITCH Switches
製造商零件編號 CF14JT220R
RES 220 OHM 5% 1/4W AXIAL
Stackpole Electronics Inc
製造商零件編號 LTL-4224
LED RED CLEAR T-1 3/4 T/H
Lite-On Inc.
製造商零件編號 14284
JUMPER M/M 4" 26AWG 30PCS
SparkFun Electronics
製造商零件編號 FIT0096
BREADBRD TERM STRIP 3.20X2.00"
DFRobot
製造商零件編號 DH-20M50055
USB AM TO USB MICRO, USB 2.0 - 1
Cvilux USA
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.