How to Run an ESP32 Zephyr Application on Espressif’s QEMU

When working with embedded systems, especially platforms like Zephyr, it’s not always practical to rely on real hardware. Maybe your dev board is in the mail, or maybe you’re trying to write tests that run in CI without any physical devices attached. That’s where QEMU comes in. QEMU is a powerful open-source emulator that can simulate various processor architectures, allowing you to build and run embedded applications as if they were running on real hardware.

Zephyr makes this even easier by providing out-of-the-box support for a number of QEMU targets, which you can see here (feel free to follow the directions given in those docs to get started with the default Zephyr QEMU). 

However, If you’re working with Espressif chips (like the ESP32 or ESP32-S3), you may want the added hardware fidelity and active maintenance of Espressif’s own fork of QEMU. This guide walks you through using both the built-in support and the Espressif fork to emulate Zephyr applications, giving you a fast, flexible way to test code before flashing a single board.

If that does not work (or you’d like to dig deeper to see how to manually build a binary and then run in Espressif’s fork of QEMU), keep reading!

Install Zephyr

You will need Zephyr to follow this tutorial. You can either follow the official Zephyr documentation to install the framework locally, or you can build a Docker image with the framework already installed for you.

If you do not want to install the Zephyr framework locally, you can download the Introduction to Zephyr GitHub repository and build the Docker image. Note that this means you will need Docker Desktop installed and running.

git clone https://github.com/ShawnHymel/introduction-to-zephyr
cd introduction-to-zephyr
docker build -t env-zephyr-espressif -f Dockerfile.espressif .

You can install the Dev Containers VS Code plugin and run the image from within VS Code. You can also run the container manually with:

Linux/macOS:

docker run --rm -it -p 3333:3333 -p 2222:22 -p 8800:8800 -v "$(pwd)"/workspace:/workspace -w /workspace env-zephyr-espressif

Windows (PowerShell):

docker run --rm -it -p 3333:3333 -p 2222:22 -p 8800:8800 -v "${PWD}\workspace:/workspace" -w /workspace env-zephyr-espressif

Note that this will map the workspace directory in the repository so that you have access to all the demo applications and libraries.

With the container running, you can open a browser and navigate to http://localhost:8800 to get an in-browser VS Code client.

Another option: install the Remote – SSH extension in VS Code and connect to the container via SSH. You should connect to root@localhost:2222, and the default root password is zephyr.

Install QEMU

While QEMU now officially supports some Xtensa architectures, I found that Espressif’s fork of QEMU supports more chips and is better kept up-to-date for such architectures. The Docker image in the previous section does not come with the Espressif fork, so we’ll need to install it manually.

Note that I assume you are doing this in the container with superuser (root) privileges. If not, you will need to use the “sudo” command in front of some of the following commands.

You’ll first need to install some dependencies:

apt-get update
apt-get install -y libpixman-1-0 libsdl2-2.0-0 libslirp0

See this page for the available pre-compiled QEMU releases from Espressif. Note the different core options for your ESP32 (Xtensa vs. RISC-V); choose the one for your particular ESP32 variant. I’ll show how to download and install v9.2.2 (as that’s what I tested) for an Xtensa-based ESP32. If your host machine uses a 64-bit, x86 processor:

cd /tmp
wget -O espressif-qemu-binary.tar.xz https://github.com/espressif/qemu/releases/download/esp-develop-9.2.2-20250228/qemu-xtensa-softmmu-esp_develop_9.2.2_20250228-x86_64-linux-gnu.tar.xz

If your host machine uses a 64-bit ARM processor (newer Macs):

cd /tmp
wget -O espressif-qemu-binary.tar.xz https://github.com/espressif/qemu/releases/download/esp-develop-9.2.2-20250228/qemu-xtensa-softmmu-esp_develop_9.2.2_20250228-aarch64-apple-darwin.tar.xz

Now, unzip the project, move it to /opt, and update the PATH:

tar -xf espressif-qemu-binary.tar.xz
mkdir -p /opt/espressif-qemu/
mv qemu/* /opt/espressif-qemu/
export PATH="/opt/espressif-qemu/bin:$PATH"

You can make this permanent (assuming you don’t delete your container) by adding the PATH update to your shell profile:

echo 'export PATH="/opt/espressif-qemu/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

With that, the Espressif fork of QEMU should be installed!

Write and Build Blink App

In your /workspace directory, write a basic blink application. If you are using the pre-made Docker image, you can use the 01-blink app. Note that we assume you are using the ESP32S3-DevKitC board for this build (as that’s what I used in my Intro to Zephyr series). 

Build the application, just as you would for deployment to real hardware:

cd /workspace/apps/01_blink
west build -p always -b esp32s3_devkitc/esp32s3/procpu -- -DDTC_OVERLAY_FILE=boards/esp32s3_devkitc.overlay

This should create a 01_blink/build/ directory. Our application binary is found at 01_blink/build/zephyr/zephyr.bin.

Create a 4 MB Binary

QEMU that comes with Zephyr (the official QEMU) optimizes for flexibility. The Espressif fork of QEMU, on the other hand, is optimized for hardware accuracy. Because ESP32 boards are expected to have 2, 4, 8, or 16 MB of attached SPI flash, you must make sure your image exactly matches one of those sizes (as the emulator pretends there is an attached flash memory chip).

To accomplish this, we’re going to create a binary of all 0s exactly 4 MB big and then copy in our zephyr.bin application over the first part of those 0s. This has the effect of padding our application binary with 0s to make it exactly 4 MB. Feel free to use a different size if you need more space for your application.

dd if=/dev/zero of=build/zephyr/zephyr_4mb.bin bs=1M count=4
dd if=build/zephyr/zephyr.bin of=build/zephyr/zephyr_4mb.bin conv=notrunc

Now, we have an application binary at build/zephyr/zephyr_4mb.bin that has been padded with 0s.

Run with QEMU

You can see the available supported machines in QEMU with:

qemu-system-xtensa -machine help

We’ll be using the esp32s3 machine for this example, as that’s what we compiled our binary for. You can get information about what options are available for that particular machine with:

qemu-system-xtensa -machine esp32s3,help

Now, run the actual binary with QEMU:

qemu-system-xtensa -nographic -machine esp32s3 -drive file=build/zephyr/zephyr_4mb.bin,if=mtd,format=raw

Let’s break down the arguments:

  • -nographic: disables graphical output and VGA console so that all console input/output is redirected to the terminal
  • -machine esp32s3: tells QEMU which hardware we want to emulate, and the esp32s3 matches the chip family we built the binary for
  • -drive: deals with mounting the flash image
    • file=build/zephyr/zephyr_4mb.bin: location of the flash image (must be 2, 4, 8, or 16 MB when working with the Espressif QEMU fok)
    • if=mtd: Interface type is “Memory Technology Device” (MTD), which is a type of flash memory (commonly used for SPI flash)
    • format=raw: treat the file as a raw binary image (not qcow2, vmdk, etc.)

Note that the emulation will likely be quite slow! This is normal. You might find that your “1 second delay” is now 4-5 seconds. This is just the nature of trying to emulate a fast microcontroller like the ESP32.

Interacting with QEMU

QEMU is not just a fire-and-forget emulator. You can interact with it! While QEMU is running your application, press ctrl+a, c to open a “monitor,” which is QEMU’s built-in command-line interface for controlling and inspecting the virtual machine.

When you see the (qemu) prompt, enter the following to get a list of available qemu commands:

help info

Of the commands listed there, you will probably find the following the most helpful:

  • info qtree – shows connected emulated devices
  • info mtree – shows the memory tree.
  • info qom-tree – shows QEMU’s internal object model hierarchy
  • info registers – shows CPU registers

When you are done with QEMU, you can exit by pressing ctrl+a, x.

Closing Thoughts

While nothing beats having real hardware, using QEMU is a great way to test your Zephyr applications if you are unable to obtain the real thing. But more importantly, it opens up a world of possibilities with test-driven development. You can easily set up automated continuous integration (CI) tests (e.g. with GitHub Actions) that use QEMU to verify your code prior to merging or deployment. QEMU even comes with a number of emulated sensors that you can try reading from (e.g. to test I2C): https://github.com/espressif/qemu/tree/esp-develop/hw/i2c.

Something that I did not demonstrate here: using QEMU with GDB. You can easily connect GDB to your application in QEMU to perform step-through debugging, register peeking, variable tracking, etc. See here for more info: https://qemu-project.gitlab.io/qemu/system/gdb.html

👉 Next time you are working on a large, complex embedded project with Zephyr, consider using QEMU to quickly test your application code!
Check out my various embedded courses (including Introduction to Zephyr) here: https://shawnhymel.com/courses/

2 thoughts on “How to Run an ESP32 Zephyr Application on Espressif’s QEMU

Leave a Reply

Your email address will not be published. Required fields are marked *