Connectivity with Zephyr - Part 1: WiFi on ESP32-S3

The ESP32 family has become a go-to platform for getting started with Zephyr. It is cheap, readily available, and offers integrated WiFi connectivity. This first part of a three-part tutorial series covers establishing a WiFi connection and obtaining an IP address via DHCP.

Target ESP32-S3-DevKitC
Zephyr version 4.3.0
Level Beginner

Tutorial Series:

  1. WiFi Connectivity (this part)
  2. HTTP Client
  3. HTTPS with TLS

Prerequisites

Hardware:

  • ESP32-S3-DevKitC (or compatible ESP32-S3 board)

Software:

  • Zephyr RTOS (tested with v4.3.0)
  • Zephyr SDK and west tool
  • Espressif toolchain and HAL module
  • A WiFi network with active DHCP server and internet access

Required Setup:

Before starting, ensure you have a working Zephyr development environment for ESP32:

  1. Zephyr Getting Started Guide: Follow the official Getting Started Guide to install Zephyr and the SDK.

  2. Espressif (ESP32) Setup: Complete the Espressif ESP32 board setup including:

    • Installing the Espressif HAL: west blobs fetch hal_espressif
    • Setting up the toolchain
  3. Verify Installation: Build and flash a simple example like samples/hello_world to confirm your environment works.

Project Structure

The project consists of these files:

wifi_app/
├── CMakeLists.txt              # Build configuration
├── Kconfig                     # Custom Kconfig options for WiFi credentials
├── prj.conf                    # Main Zephyr configuration
├── boards/
│   └── esp32s3_devkitc_procpu.conf  # Board-specific settings
└── src/
    └── main.c                  # Application code

Build System Files

Start with CMakeLists.txt:

cmake_minimum_required(VERSION 3.20.0)

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

target_sources(app PRIVATE src/main.c)

Project Configuration

The prj.conf file configures Zephyr’s networking stack for WiFi connectivity:

# Networking
CONFIG_NETWORKING=y
CONFIG_NET_IPV4=y
CONFIG_NET_DHCPV4=y
CONFIG_NET_TCP=y

# WiFi
CONFIG_WIFI=y
CONFIG_NET_L2_WIFI_MGMT=y
CONFIG_NET_MGMT=y
CONFIG_NET_MGMT_EVENT=y
CONFIG_NET_MGMT_EVENT_INFO=y

# Stack sizes for ESP32
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_NET_TX_STACK_SIZE=2048
CONFIG_NET_RX_STACK_SIZE=2048

# Network buffers
CONFIG_NET_PKT_RX_COUNT=10
CONFIG_NET_PKT_TX_COUNT=10
CONFIG_NET_BUF_RX_COUNT=20
CONFIG_NET_BUF_TX_COUNT=20

# Logging
CONFIG_LOG=y
CONFIG_NET_LOG=y

# WiFi credentials (edit these)
CONFIG_WIFI_SSID="YourSSID"
CONFIG_WIFI_PSK="YourPassword"

The CONFIG_NET_MGMT* options enable the network management API, which provides callbacks for WiFi and IP address events. The buffer counts and stack sizes are tuned for the ESP32’s memory constraints.

For the WiFi credentials, we define custom Kconfig options in Kconfig:

mainmenu "WiFi HTTP Client Application"

config WIFI_SSID
	string "WiFi SSID"
	help
	  SSID of the WiFi network to connect to.

config WIFI_PSK
	string "WiFi Password"
	help
	  WPA2-PSK password for the WiFi network.

source "Kconfig.zephyr"

Setting WiFi Credentials

You have two options for configuring the WiFi SSID and password:

Option 1: Edit prj.conf directly

Simply edit the CONFIG_WIFI_SSID and CONFIG_WIFI_PSK values in prj.conf:

CONFIG_WIFI_SSID="MyNetwork"
CONFIG_WIFI_PSK="MyPassword"

Option 2: Use menuconfig

Zephyr’s interactive configuration tool provides a menu-driven interface:

west build -t menuconfig

Navigate to the top-level menu “WiFi HTTP Client Application” to find the SSID and PSK options. Save and exit when done. This updates the build configuration without modifying prj.conf directly.

The ESP32-S3 also needs a board-specific configuration in boards/esp32s3_devkitc_procpu.conf:

CONFIG_ESP32_WIFI_STA_AUTO_DHCPV4=y

Core Concepts

Before diving into the code, let us understand some key Zephyr concepts used in this example.

Network Management Events

Zephyr’s networking stack is event-driven. Operations like WiFi connection and DHCP address assignment happen asynchronously - you initiate an operation, and the system notifies you when it completes (or fails) via callbacks. This is handled by the Network Management API (net_mgmt).

The pattern is:

  1. Define a callback function to handle specific events
  2. Register the callback with net_mgmt_add_event_callback()
  3. Initiate an operation (e.g., NET_REQUEST_WIFI_CONNECT)
  4. Your callback fires when the event occurs

Network Interfaces

A network interface (struct net_if) represents a network device - in our case, the ESP32’s WiFi radio. Zephyr abstracts different network technologies (WiFi, Ethernet, cellular) behind this common interface. We use net_if_get_default() to get the primary network interface.

Logging

Zephyr’s logging subsystem (LOG_INF, LOG_ERR, etc.) provides structured logging with module names and severity levels. The LOG_MODULE_REGISTER() macro declares a logging module with a name and default level.

WiFi Connection Code

Create src/main.c:

#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/net/wifi_mgmt.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/net_event.h>

LOG_MODULE_REGISTER(wifi_app, LOG_LEVEL_INF);

static struct net_mgmt_event_callback wifi_cb;
static struct net_mgmt_event_callback ipv4_cb;

static void wifi_event_handler(struct net_mgmt_event_callback *cb,
                               uint64_t mgmt_event, struct net_if *iface)
{
    if (mgmt_event == NET_EVENT_WIFI_CONNECT_RESULT) {
        LOG_INF("WiFi connected");
    } else if (mgmt_event == NET_EVENT_WIFI_DISCONNECT_RESULT) {
        LOG_INF("WiFi disconnected");
    }
}

static void ipv4_event_handler(struct net_mgmt_event_callback *cb,
                               uint64_t mgmt_event, struct net_if *iface)
{
    if (mgmt_event == NET_EVENT_IPV4_ADDR_ADD) {
        struct net_if_ipv4 *ipv4 = iface->config.ip.ipv4;

        if (ipv4) {
            char addr_str[NET_IPV4_ADDR_LEN];

            for (int i = 0; i < NET_IF_MAX_IPV4_ADDR; i++) {
                if (ipv4->unicast[i].ipv4.is_used) {
                    net_addr_ntop(AF_INET,
                                  &ipv4->unicast[i].ipv4.address.in_addr,
                                  addr_str, sizeof(addr_str));
                    LOG_INF("IP Address: %s", addr_str);
                }
            }
        }
    }
}

static int connect_wifi(void)
{
    struct net_if *iface = net_if_get_default();
    struct wifi_connect_req_params params = {
        .ssid = CONFIG_WIFI_SSID,
        .ssid_length = strlen(CONFIG_WIFI_SSID),
        .psk = CONFIG_WIFI_PSK,
        .psk_length = strlen(CONFIG_WIFI_PSK),
        .channel = WIFI_CHANNEL_ANY,
        .band = WIFI_FREQ_BAND_2_4_GHZ,
        .security = WIFI_SECURITY_TYPE_PSK,
    };

    LOG_INF("Connecting to %s...", CONFIG_WIFI_SSID);
    return net_mgmt(NET_REQUEST_WIFI_CONNECT, iface, &params, sizeof(params));
}

int main(void)
{
    LOG_INF("ESP32-S3 WiFi Example");

    /* Register WiFi event callback */
    net_mgmt_init_event_callback(&wifi_cb, wifi_event_handler,
                                 NET_EVENT_WIFI_CONNECT_RESULT |
                                 NET_EVENT_WIFI_DISCONNECT_RESULT);
    net_mgmt_add_event_callback(&wifi_cb);

    /* Register IPv4 event callback */
    net_mgmt_init_event_callback(&ipv4_cb, ipv4_event_handler,
                                 NET_EVENT_IPV4_ADDR_ADD);
    net_mgmt_add_event_callback(&ipv4_cb);

    /* Wait for WiFi driver to initialize */
    k_sleep(K_SECONDS(2));

    /* Connect to WiFi */
    int ret = connect_wifi();
    if (ret) {
        LOG_ERR("WiFi connect request failed: %d", ret);
    }

    return 0;
}

Code Walkthrough

Event Callbacks: We register two separate callbacks:

  • wifi_event_handler - notified when WiFi connects or disconnects
  • ipv4_event_handler - notified when DHCP assigns an IP address

WiFi Parameters: The wifi_connect_req_params structure specifies:

  • SSID and password (from Kconfig)
  • WIFI_CHANNEL_ANY - let the driver find the access point
  • WIFI_FREQ_BAND_2_4_GHZ - use 2.4 GHz band
  • WIFI_SECURITY_TYPE_PSK - WPA2-PSK authentication

Asynchronous Flow: The net_mgmt(NET_REQUEST_WIFI_CONNECT, ...) call initiates the connection but returns immediately. The actual connection happens in the background, and our callbacks are invoked when events occur. The main() function returns after initiating the connection - the callbacks continue to fire as events occur.

Building and Flashing

west build -b esp32s3_devkitc/esp32s3/procpu
west flash
west espressif monitor

Expected Output

[00:00:00.000,000] <inf> wifi_app: ESP32-S3 WiFi Example
[00:00:02.000,000] <inf> wifi_app: Connecting to YourSSID...
[00:00:05.xxx,xxx] <inf> wifi_app: WiFi connected
[00:00:06.xxx,xxx] <inf> wifi_app: IP Address: 192.168.x.x

The WiFi connection and DHCP address assignment happen asynchronously after main() returns. The callbacks continue to fire as long as the system is running.

Next Steps

With WiFi connectivity established, proceed to Part 2: HTTP Client to make HTTP requests to a web API.

Further Reading