Introduction to Zephyr Part 11: WiFi and IoT
2025-05-15 | By ShawnHymel
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:
/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().
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.
# 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.
# 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.
&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.
#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.
#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, ¶ms, 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.
#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.
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>
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:
python -m serial.tools.miniterm "<PORT>" 115200
You should see the HTML contents of example.com printed to the terminal.
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:
- Official Zephyr documentation: Networking
- Official Zephyr documentation: BSD Sockets compatible API
- Official Zephyr documentation: BSD Sockets
- Big HTTP download with Zephyr (TLS included)