Post

#39: ESP32 Internet Access in QEMU

Add internet access when emulating your ESP32 project in QEMU.

#39: ESP32 Internet Access in QEMU

Testing and debugging ESP32 applications can be challenging when you need to flash hardware repeatedly or don’t have physical devices readily available. The ESP-IDF framework includes QEMU support for the ESP32, allowing you to run and debug your applications in an emulator. However, one major limitation has been networking—QEMU doesn’t support WiFi emulation for the ESP32.

In this tutorial, I’ll show you how to enable full internet connectivity for ESP32 applications running in QEMU by using Ethernet emulation. You’ll be able to make HTTP/HTTPS requests, use MQTT, perform DNS lookups, and access any network service—all from within the QEMU environment.

Why Run ESP32 in QEMU?

Before diving into the implementation, here are some compelling reasons to use QEMU for ESP32 development:

  • Faster iteration cycles: No need to flash hardware for every code change
  • Debugging without hardware: Full GDB debugging support without a JTAG adapter
  • CI/CD integration: Run automated tests in your build pipeline
  • Multi-instance testing: Run multiple emulated devices simultaneously
  • Network testing: Test network protocols without managing physical WiFi networks

Prerequisites

You’ll need:

  • ESP-IDF version 5.4 or later installed and configured
  • QEMU for ESP32 (included with ESP-IDF)
  • Basic C/C++ knowledge and familiarity with ESP-IDF
  • A working host internet connection (QEMU will bridge through your host network)

The Challenge: Networking in QEMU

The ESP32 uses WiFi for most networking tasks, but QEMU doesn’t emulate the ESP32’s WiFi hardware. Instead, QEMU provides an OpenEth virtual Ethernet MAC that can be used to access the network. However, setting this up properly requires:

  1. Enabling the correct Kconfig options
  2. Initializing the OpenEth MAC and PHY drivers
  3. Configuring DHCP and waiting for an IP address
  4. Handling network events properly

To simplify this process, I’ve created a reusable component called qemu_internet that handles all of these details.

Getting the Component

The qemu_internet component is available in the prod_esp32_playground repository:

1
git clone https://github.com/yourusername/prod_esp32_playground.git

The component source code is located in examples/shared_components/qemu_internet/ and consists of:

  • qemu_internet.c - Implementation
  • include/qemu_internet.h - Public API header
  • CMakeLists.txt - Component build configuration

As it is just for research and dev purposes, I have not pushed this component to the ESP Component Registry. However, feel free to copy the component directly from the repo and reuse it.

Setting Up Your Project

Let’s create a new ESP-IDF project that will run in QEMU with internet access.

Step 1: Create Project Structure

Create a new ESP-IDF project with the standard structure:

1
2
3
4
5
6
my_qemu_project/
├── CMakeLists.txt
├── sdkconfig.defaults
└── main/
    ├── CMakeLists.txt
    └── main.cpp

Step 2: Configure CMakeLists.txt

Your top-level CMakeLists.txt:

1
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.16)

# Using C++ 17
set(CMAKE_CXX_STANDARD 17)

set(SDKCONFIG_DEFAULTS "sdkconfig.defaults")

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(my_qemu_project)

Step 3: Add ethernet_init Component

This component is a dependency of the qemu_internet component we will add next. Due to the current nature of the IDF build system, this dependency needs to be added at the project level.

1
idf.py add-dependency "espressif/ethernet_init"

This will create (or add to) the idf_component.yml file for your project.

Step 4: Add the qemu_internet Component

Copy the qemu_internet component directory into your project’s components directory. You may have to create the components directory if it doesn’t already exist.

Your project structure should now look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
my_qemu_project/
├── CMakeLists.txt
├── sdkconfig.defaults
├── components/
│   └── qemu_internet/
│       ├── include/
│       │   └── qemu_internet.h
│       ├── CMakeLists.txt
│       └── qemu_internet.c
└── main/
    ├── CMakeLists.txt
    ├── idf_component.yml
    └── main.cpp

Step 5: Configure sdkconfig.defaults

Create sdkconfig.defaults with the required Kconfig settings:

1
2
3
4
5
6
7
8
9
# Enable Ethernet support for QEMU
CONFIG_ETH_ENABLED=y
CONFIG_ETH_USE_ESP32_EMAC=y
CONFIG_ETH_USE_OPENETH=y

# Disable hardware crypto (not supported in QEMU)
CONFIG_MBEDTLS_HARDWARE_AES=n
CONFIG_MBEDTLS_HARDWARE_SHA=n
CONFIG_MBEDTLS_HARDWARE_MPI=n

These settings are crucial:

  • The first three enable the OpenEth driver that QEMU provides
  • The crypto settings prevent crashes when using HTTPS/TLS (QEMU doesn’t emulate the ESP32’s crypto hardware properly)

Step 6: Configure main/CMakeLists.txt

In your main/CMakeLists.txt, declare the qemu_internet component as a dependency:

1
2
idf_component_register(SRCS "main.cpp"
                       PRIV_REQUIRES esp-tls esp_event esp_http_client esp_netif qemu_internet)

Step 7: Implement Your Application

Here’s a complete example in main/main.c that connects to the internet and makes an HTTPS request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <stdio.h>
#include <string.h>

#include "esp_crt_bundle.h"
#include "esp_event.h"
#include "esp_http_client.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "lwip/netdb.h"
#include "qemu_internet.h"

static const char* TAG = "app";

// Simple HTTP event handler
esp_err_t http_event_handler(esp_http_client_event_t* evt) {
  switch (evt->event_id) {
    case HTTP_EVENT_ON_DATA:
      if (!esp_http_client_is_chunked_response(evt->client)) {
        printf("%.*s", evt->data_len, (char*)evt->data);
      }
      break;
    default:
      break;
  }
  return ESP_OK;
}

extern "C" void app_main(void) {
  ESP_LOGI(TAG, "Starting QEMU internet example");

  // Step 1: Initialize networking stack
  ESP_ERROR_CHECK(esp_netif_init());

  // Step 2: Create default event loop
  ESP_ERROR_CHECK(esp_event_loop_create_default());

  // Step 3: Connect to internet (blocks until IP obtained)
  ESP_LOGI(TAG, "Connecting to internet...");
  esp_err_t ret = qemu_internet_connect();
  if (ret != ESP_OK) {
    ESP_LOGE(TAG, "Failed to connect: %s", esp_err_to_name(ret));
    return;
  }
  ESP_LOGI(TAG, "Connected! IP obtained via DHCP");

  // Step 4: Test DNS resolution
  ESP_LOGI(TAG, "Testing DNS resolution...");
  struct addrinfo hints = {
      .ai_family = AF_INET,
      .ai_socktype = SOCK_STREAM,
  };
  struct addrinfo* res = NULL;

  int err = getaddrinfo("www.example.com", "80", &hints, &res);
  if (err == 0 && res != NULL) {
    struct sockaddr_in* addr = (struct sockaddr_in*)res->ai_addr;
    ESP_LOGI(TAG, "DNS resolved: %s", inet_ntoa(addr->sin_addr));
    freeaddrinfo(res);
  }

  // Step 5: Make an HTTPS request
  ESP_LOGI(TAG, "Making HTTPS request...");
  esp_http_client_config_t config = {
      .url = "https://www.howsmyssl.com/a/check",
      .event_handler = http_event_handler,
      .crt_bundle_attach = esp_crt_bundle_attach,
  };

  esp_http_client_handle_t client = esp_http_client_init(&config);
  ret = esp_http_client_perform(client);

  if (ret == ESP_OK) {
    int status = esp_http_client_get_status_code(client);
    ESP_LOGI(TAG, "HTTPS request completed, status = %d", status);
  }
  else {
    ESP_LOGE(TAG, "HTTPS request failed: %s", esp_err_to_name(ret));
  }

  esp_http_client_cleanup(client);

  // Keep running
  while (1) {
    ESP_LOGI(TAG, "Application running...");
    vTaskDelay(pdMS_TO_TICKS(5000));
  }
}

Building and Running

Build the Project

1
2
cd my_qemu_project
idf.py build

Run in QEMU

Launch QEMU with the monitor interface:

1
idf.py qemu monitor

This will run your project in the QEMU emulator and, if configured successfully, will show a successful DNS resolution for example.com and will also successfully query the JSON endpoint of howsmyssl.com which will show the configured cipher suites.

How It Works

Let’s break down what’s happening behind the scenes:

Network Initialization

1
2
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());

These two lines set up the ESP-IDF networking stack and create an event loop for handling network events.

Connecting via qemu_internet

1
esp_err_t ret = qemu_internet_connect();

This single function call:

  1. Initializes the OpenEth MAC and PHY drivers for QEMU
  2. Creates an Ethernet network interface
  3. Starts the DHCP client
  4. Registers handlers for IP address events
  5. Blocks until an IP address is obtained
  6. Returns ESP_OK when ready to use the network

The blocking behavior is intentional—it ensures your application doesn’t try to use the network before it’s ready.

Using Standard ESP-IDF APIs

Once connected, you can use any ESP-IDF networking components:

  • HTTP/HTTPS: esp_http_client
  • MQTT: mqtt_client
  • Sockets: Standard POSIX sockets or esp_netif APIs
  • DNS: getaddrinfo(), gethostbyname()
  • NTP: Time synchronization
  • mDNS: Service discovery

All of these work exactly as they would on physical hardware.

Limitations and Considerations

Keep these limitations in mind:

  1. QEMU is an emulator: Performance doesn’t match real hardware
  2. No WiFi: Only Ethernet networking is available
  3. Hardware crypto disabled: HTTPS/TLS is slower without hardware acceleration
  4. Not all peripherals work: Some ESP32 hardware features aren’t emulated
  5. Different network environment: QEMU uses NAT/bridging, which may behave differently than WiFi

Despite these limitations, QEMU is invaluable for development, testing, and CI/CD workflows.

Use Cases

This setup is useful for:

  • Automated testing: Run network protocol tests in CI/CD pipelines
  • Development without hardware: Build and test network features without ESP32 boards
  • Education: Teach ESP32 networking without requiring hardware for every student
  • Multi-device scenarios: Test interactions between multiple emulated devices
  • Reproducible bugs: Create isolated, reproducible test environments

Troubleshooting

HTTPS/TLS crashes

Ensure hardware crypto is disabled:

1
2
3
CONFIG_MBEDTLS_HARDWARE_AES=n
CONFIG_MBEDTLS_HARDWARE_SHA=n
CONFIG_MBEDTLS_HARDWARE_MPI=n

Build errors about ethernet_init

The component depends on the ethernet_init helper component from ESP-IDF examples. It must be installed at the root level of the project using:

1
idf.py add-dependency "espressif/ethernet_init"

Conclusion

Running ESP32 applications in QEMU with full internet access opens up new possibilities for development and testing. The qemu_internet component simplifies the complex Ethernet initialization, giving you a single function call that “just works.”

This ties into the Deterministic and Tested pillars of production.

Whether you’re developing IoT applications, building automated test suites, or teaching embedded networking, this approach provides a fast, reliable way to test network functionality without physical hardware.

The complete source code for the qemu_internet component and working examples can be found in the prod_esp32_playground repository. Feel free to use it in your own projects, and happy coding!

© Kevin Sidwar